Orleans 中的序列化
Orleans 中所使用的序列化大致有兩種:
- 精細度呼叫序列化 - 用來序列化傳入和傳出精細度的物件。
- 精細度儲存序列化 - 用來序列化進出儲存體系統的物件。
本文中大部分都是透過 Orleans 中包含的序列化架構,專門用來呼叫序列化。 精細度儲存序列化程式一節討論精細度儲存體序列化。
使用 Orleans 序列化
Orleans 包含進階且可延伸的序列化架構,可稱為 Orleans。序列化。 Orleans 中包含的序列化架構是設計來符合下列目標:
- 高效能 - 序列化程式是針對效能而設計並最佳化。 此簡報提供更多詳細資料。
- 高逼真度 - 序列化程式忠實地代表大部分 .NET 的型別系統,包括對泛型、多型、繼承階層、物件識別和循環圖形的支援。 不支援指標,因為它們不具有跨處理程序的可攜性質。
- 彈性 - 可以藉由建立代理或委派給外部序列化程式庫,例如 System.Text.Json、Newtonsoft.Json 和 Google.Protobuf,來自訂序列化程式以支援第三方程式庫。
- 版本容錯 - 序列化程式可讓應用程式型別隨著時間演進,支援:
- 新增和移除成員
- 子類別
- 數值擴大和縮小 (例如:
int
至/從long
、float
至/從double
) - 重新命名型別
型別的高逼真度表示法對於序列化程式相當不常見,因此有些點需要進一步解釋:
動態型別和任意多型:Orleans 不會對可以傳入 Grain 呼叫的型別強制執行限制,而且會維持實際資料型別的動態本質。 這表示,例如,如果宣告在 Grain 介面中的方法接受 IDictionary,但是在執行階段,傳送方傳遞 SortedDictionary<TKey,TValue>,接收方實際得到的會是
SortedDictionary
(雖然「靜態合約」 / Grain 介面未指定這項行為)。維持物件識別:如果相同物件在 Grain 呼叫的引數中傳遞多個型別,或間接從引數指向物件多次,則 Orleans 只會序列化物件一次。 在接收方端,Orleans 會正確還原所有參考,使相同物件的兩個指標在還原序列化之後仍然指向相同的物件。 在類似以下的案例中,物件識別的保留非常重要。 假設精細度 A 將具有 100 個項目的字典傳送精細度 B,該字典中有 10 個索引鍵指向位於 A 端的同一個物件
obj
。 如果沒有保留物件識別,則 B 會收到 100 個項目的字典,其中 10 個索引鍵指向 10 個不同的obj
複本。 使用物件識別保留時,B 端的字典看起來與指向單一物件obj
的 10 個索引鍵完全相同。 請注意,由於 .NET 中的預設字串雜湊程式碼實作是隨機化個別處理程序,因此字典和雜湊集 (舉例來說) 中的值順序可能不會保留。
為了支援版本容錯,序列化程式需要開發人員明確說明哪些型別和成員已序列化。 我們已嘗試盡可能簡化這個動作。 您必須使用 Orleans.GenerateSerializerAttribute 標記所有可序列化的型別,以指示 Orleans 針對您的型別產生序列化程式的程式碼。 完成此動作之後,您可以使用內含的程式碼修正,將所需的 Orleans.IdAttribute 新增至型別上可序列化的成員,如下所示:
以下是 Orleans 中可序列化型別的範例,示範如何套用屬性。
[GenerateSerializer]
public class Employee
{
[Id(0)]
public string Name { get; set; }
}
Orleans 支援繼承,而且會個別序列化階層中的個別層,使其具有不同的成員識別碼。
[GenerateSerializer]
public class Publication
{
[Id(0)]
public string Title { get; set; }
}
[GenerateSerializer]
public class Book : Publication
{
[Id(0)]
public string ISBN { get; set; }
}
在上述程式碼中,請注意 Publication
和 Book
都有成員與 [Id(0)]
,即使 Book
衍生自 Publication
。 這是 Orleans 中的建議做法,因為成員識別碼的範圍是繼承層級,而不是整體型別。 成員可以從 Publication
和 Book
獨立新增和移除,但一旦部署應用程式而無特殊考慮,就無法將新的基底類別插入階層中。
Orleans 也支援使用 internal
、private
和 readonly
成員序列化型別,例如此範例型別:
[GenerateSerializer]
public struct MyCustomStruct
{
public MyCustom(int intProperty, int intField)
{
IntProperty = intProperty;
_intField = intField;
}
[Id(0)]
public int IntProperty { get; }
[Id(1)] private readonly int _intField;
public int GetIntField() => _intField;
public override string ToString() => $"{nameof(_intField)}: {_intField}, {nameof(IntProperty)}: {IntProperty}";
}
依預設,Orleans 會透過編碼全名的方式,序列化您的型別。 您可以透過新增 Orleans.AliasAttribute 的方式覆寫此設定。 這樣做會導致您的型別使用具有復原性的名稱來序列化,以重新命名基礎類別或在組件之間移動。 型別別名以全域範圍為使用範圍,而且您無法在一個應用程式中擁有兩個具有相同值的別名。 若是泛型型別,別名值必須包含前面加上倒引號的泛型參數數目,例如 MyGenericType<T, U>
可以擁有別名 [Alias("mytype`2")]
。
序列化 record
型別
依預設,在記錄的主要建構函式中定義的成員具有隱含識別碼。 換句話說,Orleans 支援序列化 record
型別。 這表示,您無法變更已部署型別的參數順序,因為這樣會破壞與舊版應用程式之間的相容性 (在輪流升級的情況下),以及在儲存體與串流中,會破壞與該型別已序列化執行個體之間的相容性。 在記錄型別的主體中定義的成員不會與主要建構函式參數共用識別。
[GenerateSerializer]
public record MyRecord(string A, string B)
{
// ID 0 won't clash with A in primary constructor as they don't share identities
[Id(0)]
public string C { get; init; }
}
如果您不想自動包含主要建構函式參數做為可序列化的欄位,您可以使用 [GenerateSerializer(IncludePrimaryConstructorParameters = false)]
。
適用於序列化外部型別的替代項目
有時候,您可能需要在未完全控制的 Grain 之間傳遞型別。 在這些情況下,手動轉換應用程式程式碼中的自訂定義型別並不實際。 Orleans 以替代型別的形式提供這些情況的解決方案。 替代項目會代替其目標型別進行序列化,具有轉換目標型別的功能。 請參考下列有關外部型別、其對應替代項目及轉換器的範例:
// This is the foreign type, which you do not have control over.
public struct MyForeignLibraryValueType
{
public MyForeignLibraryValueType(int num, string str, DateTimeOffset dto)
{
Num = num;
String = str;
DateTimeOffset = dto;
}
public int Num { get; }
public string String { get; }
public DateTimeOffset DateTimeOffset { get; }
}
// This is the surrogate which will act as a stand-in for the foreign type.
// Surrogates should use plain fields instead of properties for better performance.
[GenerateSerializer]
public struct MyForeignLibraryValueTypeSurrogate
{
[Id(0)]
public int Num;
[Id(1)]
public string String;
[Id(2)]
public DateTimeOffset DateTimeOffset;
}
// This is a converter that converts between the surrogate and the foreign type.
[RegisterConverter]
public sealed class MyForeignLibraryValueTypeSurrogateConverter :
IConverter<MyForeignLibraryValueType, MyForeignLibraryValueTypeSurrogate>
{
public MyForeignLibraryValueType ConvertFromSurrogate(
in MyForeignLibraryValueTypeSurrogate surrogate) =>
new(surrogate.Num, surrogate.String, surrogate.DateTimeOffset);
public MyForeignLibraryValueTypeSurrogate ConvertToSurrogate(
in MyForeignLibraryValueType value) =>
new()
{
Num = value.Num,
String = value.String,
DateTimeOffset = value.DateTimeOffset
};
}
在上述程式碼中:
MyForeignLibraryValueType
不是您可以控制的型別,定義於取用程式庫中。MyForeignLibraryValueTypeSurrogate
是對應至MyForeignLibraryValueType
的替代型別。- RegisterConverterAttribute 指定
MyForeignLibraryValueTypeSurrogateConverter
做為在兩個型別之間對應的轉換器。 該類別是 IConverter<TValue,TSurrogate> 介面的實作。
Orleans 支援序列化型別階層 (衍生自其他型別的型別) 中的型別。 如果外部型別可能會出現在型別階層 (例如做為您自有型別之一的基底類別),您必須額外實作 Orleans.IPopulator<TValue,TSurrogate> 介面。 請考慮下列範例:
// The foreign type is not sealed, allowing other types to inherit from it.
public class MyForeignLibraryType
{
public MyForeignLibraryType() { }
public MyForeignLibraryType(int num, string str, DateTimeOffset dto)
{
Num = num;
String = str;
DateTimeOffset = dto;
}
public int Num { get; set; }
public string String { get; set; }
public DateTimeOffset DateTimeOffset { get; set; }
}
// The surrogate is defined as it was in the previous example.
[GenerateSerializer]
public struct MyForeignLibraryTypeSurrogate
{
[Id(0)]
public int Num;
[Id(1)]
public string String;
[Id(2)]
public DateTimeOffset DateTimeOffset;
}
// Implement the IConverter and IPopulator interfaces on the converter.
[RegisterConverter]
public sealed class MyForeignLibraryTypeSurrogateConverter :
IConverter<MyForeignLibraryType, MyForeignLibraryTypeSurrogate>,
IPopulator<MyForeignLibraryType, MyForeignLibraryTypeSurrogate>
{
public MyForeignLibraryType ConvertFromSurrogate(
in MyForeignLibraryTypeSurrogate surrogate) =>
new(surrogate.Num, surrogate.String, surrogate.DateTimeOffset);
public MyForeignLibraryTypeSurrogate ConvertToSurrogate(
in MyForeignLibraryType value) =>
new()
{
Num = value.Num,
String = value.String,
DateTimeOffset = value.DateTimeOffset
};
public void Populate(
in MyForeignLibraryTypeSurrogate surrogate, MyForeignLibraryType value)
{
value.Num = surrogate.Num;
value.String = surrogate.String;
value.DateTimeOffset = surrogate.DateTimeOffset;
}
}
// Application types can inherit from the foreign type, assuming they're not sealed
// since Orleans knows how to serialize it.
[GenerateSerializer]
public sealed class DerivedFromMyForeignLibraryType : MyForeignLibraryType
{
public DerivedFromMyForeignLibraryType() { }
public DerivedFromMyForeignLibraryType(
int intValue, int num, string str, DateTimeOffset dto) : base(num, str, dto)
{
IntValue = intValue;
}
[Id(0)]
public int IntValue { get; set; }
}
版本設定規則
若要支援版本容錯,開發人員必須在修改型別時遵循一組規則。 如果開發人員熟悉 Google 通訊協定緩衝區 (Protobuf) 等系統,則這些規則會很熟悉。
複合型別 (class
& struct
)
- 支援繼承,但不支援修改物件的繼承階層。 類別的基底類別無法新增、變更為另一個類別或移除。
- 除了某些數值型別的例外狀況之外,如下面數值一節所述,無法變更欄位型別。
- 您可以在繼承階層中的任何時間點新增或移除欄位。
- 欄位識別碼無法變更。
- 欄位識別碼對於型別階層中的每個層級都必須是唯一的,但可以在基底類別和子類別之間重複使用。 例如,
Base
類別可以宣告具有識別碼0
的欄位,而不同的欄位可以使用具有相同識別碼0
的Sub : Base
來宣告。
數值
- 無法變更數值欄位的正負號狀態。
int
和uint
之間的轉換無效。
- 數值欄位的寬度可以變更。
- 例如:支援從
int
轉換為long
或從ulong
轉換為ushort
。 - 如果欄位的執行階段值會造成溢位,則會擲回縮小寬度的轉換。
- 只有在執行階段的值小於
ushort.MaxValue
時,才支援從ulong
轉換為ushort
。 - 只有在執行階段值介於
float.MinValue
和float.MaxValue
之間時,才支援從double
轉換為float
。 - 同樣地,對於
decimal
,其範圍比double
和float
都窄。
- 只有在執行階段的值小於
- 例如:支援從
複製器
Orleans 預設可提升安全性。 這包括某些並行錯誤類別的安全性。 特別是,Orleans 預設會立即複製傳入精細度呼叫的物件。 這項複製是由 Orleans 輔助處理的。序列化以及將 Orleans.CodeGeneration.GenerateSerializerAttribute 套用至型別時,Orleans 也會產生該型別的複製器。 Orleans 將會避免複製使用 ImmutableAttribute 標記的型別或個別成員。 如需詳細資料,請參閱Orleans 中的不可變型別序列化。
序列化最佳做法
✅請務必利用
[Alias("my-type")]
屬性提供型別別名。 重新命名具有別名的型別不會破壞相容性。❌請勿將
record
變更為一般class
,反之亦然。 記錄和類別不會以完全相同的方式表示,因為記錄除了一般成員之外,還有主要建構函式成員,因此這兩者不可互換。❌請勿將新型別新增至現有型別階層以做為可序列化的型別。 您不得將新的基底類別新增至現有的型別。 您可以將新的子類別安全地新增至現有的型別。
✅請務必將 SerializableAttribute 的用法取代為 GenerateSerializerAttribute 及對應的 IdAttribute 宣告。
✅每個型別的所有成員識別碼務必從零開始。 子類別及其基底類別中的識別碼可以安全地重疊。 下列範例中的兩個屬性都有等於
0
的識別碼。[GenerateSerializer] public sealed class MyBaseClass { [Id(0)] public int MyBaseInt { get; set; } } [GenerateSerializer] public sealed class MySubClass : MyBaseClass { [Id(0)] public int MyBaseInt { get; set; } }
✅請務必視需要擴大數值成員的型別。 您可以將
sbyte
擴大為short
、int
或long
。- 您可以縮小數值成員的型別,但如果縮小的型別無法正確表示觀察到的值,則會導致發生執行階段例外狀況。 例如,
int.MaxValue
無法以short
欄位表示,因此如果遇到這類的值,將int
欄位縮小為short
可能會導致發生執行階段例外狀況。
- 您可以縮小數值成員的型別,但如果縮小的型別無法正確表示觀察到的值,則會導致發生執行階段例外狀況。 例如,
❌請勿變更數值型別成員的正負號狀態。 例如,您不得將成員的型別從
uint
變更為int
,或從int
變更為uint
。
Grain 儲存體序列化程式
Orleans 包含有適用於 Grain、由提供者支援的持續性模型,可以透過 State 屬性存取,或是將一或多個 IPersistentState<TState> 值插入至您的 Grain 中。 在 Orleans 7.0 之前,每個提供者都有不同的機制可以設定序列化。 在 Orleans 7.0 中,現在有一般用途的 Grain 狀態序列化程式介面,即 IGrainStorageSerializer,提供一致的方式讓每個提供者自訂狀態序列化。 支援的儲存體提供者會實作一種模式,其中涉及在提供者的選項類別上設定 IStorageProviderSerializerOptions.GrainStorageSerializer 屬性,例如:
- DynamoDBStorageOptions.GrainStorageSerializer
- AzureBlobStorageOptions.GrainStorageSerializer
- AzureTableStorageOptions.GrainStorageSerializer
- GrainStorageSerializer
Grain 儲存體序列化目前的預設設定是使用 Newtonsoft.Json
來序列化狀態。 您可以在設定時修改該屬性來取代這個設定。 下列範例示範使用 OptionsBuilder<TOptions>:
siloBuilder.AddAzureBlobGrainStorage(
"MyGrainStorage",
(OptionsBuilder<AzureBlobStorageOptions> optionsBuilder) =>
{
optionsBuilder.Configure<IMySerializer>(
(options, serializer) => options.GrainStorageSerializer = serializer);
});
如需詳細資訊,請參閱 OptionsBuilder API。
Orleans 擁有進階且可延伸的序列化架構。 Orleans 會序列化傳入的 Grain 要求和回應訊息,以及 Grain 持續性狀態的物件。 在此架構中,Orleans 會針對這些資料型別自動產生序列化程式碼。 除了針對 .NET 已可序列化的型別產生更有效率的序列化/還原序列化之外,Orleans 也會嘗試針對 .NET 無法序列化但在 Grain 介面中使用的型別產生序列化程式。 此架構也包含一組高效率的內建序列化程式,適用於常用的型別:清單、字典、字串、基本型別、陣列等。
Orleans 序列化程式具備兩項重要功能,使其能夠在眾多其他協力廠商的序列化架構中脫穎而出:動態型別/任意多型和物件識別。
動態型別和任意多型:Orleans 不會對可以傳入 Grain 呼叫的型別強制執行限制,而且會維持實際資料型別的動態本質。 這表示,例如,如果宣告在 Grain 介面中的方法接受 IDictionary,但是在執行階段,傳送方傳遞 SortedDictionary<TKey,TValue>,接收方實際得到的會是
SortedDictionary
(雖然「靜態合約」 / Grain 介面未指定這項行為)。維持物件識別:如果相同物件在 Grain 呼叫的引數中傳遞多個型別,或間接從引數指向物件多次,則 Orleans 只會序列化物件一次。 在接收方端,Orleans 會正確還原所有參考,使相同物件的兩個指標在還原序列化之後仍然指向相同的物件。 在類似以下的案例中,物件識別的保留非常重要。 假設精細度 A 將具有 100 個項目的字典傳送精細度 B,該字典中有 10 個索引鍵指向位於 A 端的同一個物件 obj。 如果沒有保留物件識別,則 B 會收到 100 個項目的字典,其中 10 個索引鍵指向 10 個不同的 obj 複本。使用物件識別保留時,B 端的字典看起來與指向單一物件 obj 的 10 個索引鍵完全相同。
上述兩種行為是由標準 .NET 二進位序列化程式所提供,因此在 Orleans 中支援此標準且熟悉的行為對我們而言非常重要。
產生的序列化程式
Orleans 使用下列規則決定要產生的序列化程式。 規則如下:
- 在參考核心 Orleans 程式庫的所有組件中掃描所有型別。
- 在這些組件中:針對在 Grain 介面方法簽章或狀態類別簽章中直接參考的型別,或是針對使用 SerializableAttribute 標記的任何型別,產生序列化程式。
- 此外,Grain 介面或實作專案可以指向任意型別以用於序列化的產生,透過新增 KnownTypeAttribute 或 KnownAssemblyAttribute 組件層級屬性,告知程式碼產生器針對組件內特定型別或所有符合資格的型別,產生序列化程式。 如需組件層級屬性的詳細資訊,請參閱在組件層級套用屬性。
後援序列化
Orleans 支援在執行階段傳輸任意型別,因此內建程式碼產生器無法判斷將會事先傳輸的整組型別。 此外,某些型別無法產生序列化程式,因其無法存取 (例如 private
) 或具有無法存取的欄位 (例如 readonly
)。 因此,非預期或無法事先產生序列化程式的型別有 just-in-time 序列化型別的需求。 負責這些型別的序列化程式稱為後援序列化程式。 Orleans 隨附兩個後援序列化程式:
- Orleans.Serialization.BinaryFormatterSerializer,其使用 .NET 的 BinaryFormatter;和
- Orleans.Serialization.ILBasedSerializer,在執行階段發出 CIL 指令以建立序列化程式,該程式利用 Orleans 的序列化架構序列化每個欄位。 這表示,如果無法存取的型別
MyPrivateType
包含有具有自訂序列化程式的欄位MyType
,則該自訂序列化程式將用來序列化此型別。
您可以使用用戶端的 ClientConfiguration 和 Silo 的 GlobalConfiguration 上的 FallbackSerializationProvider 屬性設定後援序列化程式。
// Client configuration
var clientConfiguration = new ClientConfiguration();
clientConfiguration.FallbackSerializationProvider =
typeof(FantasticSerializer).GetTypeInfo();
// Global configuration
var globalConfiguration = new GlobalConfiguration();
globalConfiguration.FallbackSerializationProvider =
typeof(FantasticSerializer).GetTypeInfo();
或者,可以在 XML 組態中指定後援序列化提供者:
<Messaging>
<FallbackSerializationProvider
Type="GreatCompany.FantasticFallbackSerializer, GreatCompany.SerializerAssembly"/>
</Messaging>
BinaryFormatterSerializer 是預設的後援序列化程式。
警告
使用 BinaryFormatter
進行二進位序列化可能很危險。 如需詳細資訊,請參閱 BinaryFormatter 安全性指南 和 BinaryFormatter 移轉指南。
例外狀況序列化
例外狀況使用後援序列化程式進行序列化。 若使用預設組態,後援序列化程式是 BinaryFormatter
,因此必須遵循 ISerializable 模式,才能確保正確序列化例外狀況型別中的所有屬性。
以下範例是已正確實作序列化的例外狀況型別:
[Serializable]
public class MyCustomException : Exception
{
public string MyProperty { get; }
public MyCustomException(string myProperty, string message)
: base(message)
{
MyProperty = myProperty;
}
public MyCustomException(string transactionId, string message, Exception innerException)
: base(message, innerException)
{
MyProperty = transactionId;
}
// Note: This is the constructor called by BinaryFormatter during deserialization
public MyCustomException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
MyProperty = info.GetString(nameof(MyProperty));
}
// Note: This method is called by BinaryFormatter during serialization
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue(nameof(MyProperty), MyProperty);
}
}
序列化最佳做法
序列化在 Orleans 中提供兩個主要用途:
- 做為在執行階段 Grain 和用戶端之間傳輸資料的電傳格式。
- 做為保存長期資料以供稍後擷取的儲存格式。
Orleans 產生的序列化程式,由於具備彈性、效能及多樣化,因此適合第一個用途。 但因為不是明確的版本相容,不適合第二個用途。 建議使用者設定版本相容的序列化程式,例如適用於持續性資料的通訊協定緩衝區。 「通訊協定緩衝區」透過 Microsoft.Orleans.OrleansGoogleUtils NuGet 套件中的 Orleans.Serialization.ProtobufSerializer
支援。 所選特定序列化程式的最佳做法應該是用來確保版本相容。 您可以使用 SerializationProviders
組態屬性設定協力廠商的序列化程式,如上所述。