在Orleans中的串行化

中 Orleans所使用的串行化大致有兩種:

  • 粒子呼叫序列化:用來序列化傳入和傳出粒子的物件。
  • 粒紋儲存串行化:用來將物件串行化至儲存系統及從儲存系統。

本文大部分著重於透過 中包含的 Orleans串行化架構進行粒紋呼叫串行化。 穀物儲存串行化程式一節討論糧食儲存串行化。

使用 Orleans 序列化

Orleans 包含一個被稱為Orleans.Serialization的進階且可擴充的序列化架構。 中包含的 Orleans 串行化架構是設計來符合下列目標:

  • 高效能:串行化程式已針對效能進行設計和優化。 此簡報提供更多詳細數據。
  • 高逼真度:串行化程式忠實地代表大部分 。NET 的類型系統,包括對泛型、多型、繼承階層、對象識別和循環圖形的支援。 不支援指標,因為它們無法跨進程移植。
  • 彈性:你可以自訂序列化器以支援第三方函式庫,方法是建立 替代函 式庫或委派給外部序列化函式庫,如 System.Text.Json、 Newtonsoft.Json、 MessagePack 和 Google.Protobuf。
  • 版本容錯:串行化程式可讓應用程式類型隨著時間演進,支援:
    • 新增和移除成員
    • 子類別化
    • 數值擴大和縮小 (例如, int 從 到/從 long, float 到/從 double)
    • 重新命名類型

序列化器的高保真度型別表示頗不常見,因此有些點需要進一步說明:

  1. 動態類型和任意多型: Orleans 不會對傳入粒紋呼叫的類型強制執行限制,並維護實際數據類型的動態本質。 例如,如果粒紋介面中的方法宣告為接受 IDictionary,但在運行時間,傳送者會傳遞 SortedDictionary<TKey,TValue>,接收者確實會取得 SortedDictionary (即使 “static contract”/grain 介面未指定此行為)。

  2. 維護對象識別:如果在 Grain 呼叫的參數中多次傳遞相同的物件,或從參數中被多次間接指向,Orleans 則只會將它序列化一次。 在接收端,Orleans 能夠正確還原所有參考,以確保在反序列化之後,相同物件的兩個指標仍然指向相同的物件。 在如下情況中,保留物件識別很重要:想像一下,A 將具有 100 個專案的字典傳送至粒紋 B,而字典中的 10 個索引鍵指向 A 端的相同物件 obj。 如果不保留物件身份,B 會收到含有 100 個條目的字典,其中 10 個鍵指向 obj 的 10 個不同複製品。 在保留對象識別的情況下,B 端的字典看起來與 A 端完全相同,這 10 個索引鍵都指向單一物件 obj。 請注意,由於 .NET 中的預設字串哈希程式代碼實作是隨機化每個進程,因此字典和哈希集中的值順序可能不會保留。

為了支援版本容錯,串行化程式會要求您明確說明要串行化哪些類型和成員。 我們試圖使這盡可能無痛。 使用標記所有可串行化的型 Orleans.GenerateSerializerAttribute 別,以指示 Orleans 為您的類型產生串行化程序代碼。 完成此動作之後,您可以使用包含的程式代碼修正,將所需的 Orleans.IdAttribute 新增至類型上可串行化的成員,如下所示:

當包含的類型在其成員上不包含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 MyCustomStruct(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)]。

訊息包序列化

從 8.2 開始,你可以使用 Orleans 作為的外部序列化器。 MessagePack 是一種高效能的二進位序列化格式,能產生比 JSON 更小的有效載荷,同時維持快速的序列化與反序列化速度。

何時使用 MessagePack

考慮在以下情況下使用 MessagePack :

  • 你需要與支援 MessagePack 的 non-.NET 系統互通
  • 你已經有用 MessagePack 屬性標註的類型([MessagePackObject], [Key])
  • 你希望有效載荷大小比基於 JSON 的序列化器更小
  • 你需要一個標準化的二進位格式

對大多數 Orleans 應用程式來說,建議使用預設 Orleans 序列化器,因為它提供更高的保真度(.NET 類型系統支援)、物件身份保存及自動序列化器產生。

安裝 MessagePack 序列化套件

將 MessagePack 序列化套件加入您的專案:

dotnet add package Microsoft.Orleans.Serialization.MessagePack

設定 MessagePack 序列化

要設定 MessagePack 序列化,請使用 AddMessagePackSerializer 擴充方法:

public static void ConfigureMessagePackBasic(string[] args)
{
    var builder = Host.CreateApplicationBuilder(args);

    builder.UseOrleans(siloBuilder =>
    {
        siloBuilder.UseLocalhostClustering();
        siloBuilder.Services.AddSerializer(serializerBuilder => serializerBuilder.AddMessagePackSerializer(
            isSerializable: type => type.Namespace?.StartsWith("MyApp.Messages") == true,
            isCopyable: type => false,
            messagePackSerializerOptions: null
        ));
    });
}

代理控制 isSerializable MessagePack 序列化器處理哪些類型。 不符合此謂詞的類型會回退到預設 Orleans 序列化器。

MessagePack編解碼器選項

你可以使用以下 MessagePackCodecOptions工具來設定 MessagePack 序列化器:

房產 類型 預設 Description
SerializerOptions MessagePackSerializerOptions MessagePackSerializerOptions.Standard 用於進行序列化的 MessagePack 序列化器選項。
AllowDataContractAttributes bool false 當 true 時,標記為 [DataContract] 的型別可以使用 MessagePack 進行序列化。
IsSerializableType Func<Type, bool?> null 一個代理用來判斷某個型別是否應該由 MessagePack 序列化。
IsCopyableType Func<Type, bool?> null 一個代表用來判斷某個型別是否應該由 MessagePack 複製。

範例:用選項配置

public static void ConfigureMessagePackWithOptions(string[] args)
{
    var builder = Host.CreateApplicationBuilder(args);

    builder.UseOrleans(siloBuilder =>
    {
        siloBuilder.UseLocalhostClustering();
        siloBuilder.Services.AddSerializer(serializerBuilder => serializerBuilder.AddMessagePackSerializer(
            isSerializable: type => type.GetCustomAttribute<MessagePackObjectAttribute>() != null,
            isCopyable: type => false,
            configureOptions: options => options.Configure(opts =>
            {
                opts.SerializerOptions = MessagePackSerializerOptions.Standard
                    .WithCompression(MessagePackCompression.Lz4BlockArray);
                opts.AllowDataContractAttributes = true;
            })
        ));
    });
}

定義訊息包類型

由 MessagePack 序列化的類型應使用 MessagePack 屬性:

[MessagePackObject]
public class OrderMessage
{
    [Key(0)]
    public string OrderId { get; set; }

    [Key(1)]
    public decimal Amount { get; set; }

    [Key(2)]
    public DateTime CreatedAt { get; set; }

    [Key(3)]
    public List<string> Items { get; set; }
}

接著你可以在晶粒介面中使用以下類型:

public interface IOrderGrain : IGrainWithStringKey
{
    Task<OrderMessage> GetOrder();
    Task PlaceOrder(OrderMessage order);
}

串列器比較

特徵 / 功能 Orleans 原生 MessagePack System.Text.Json
格式 Binary Binary 文字(JSON)
.NET 型別忠實度 非常好 好 限制
物件身分識別 Yes 否 否
承載大小 小 最小 最大
跨平台 僅限 .NET 任何 MessagePack 用戶端 任何 JSON 用戶端
版本容差 Yes Yes Yes
需要設定 None 明確屬性 明確屬性

序列化外部類型的代理

有時候,您可能需要在沒有完整控制權的粒紋之間傳遞類型。 在這些情況下,在應用程式碼中手動轉換至或從自定義型別可能不切實際。 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的欄位,而Sub : Base類別可以宣告具有相同識別碼的不同欄位。 0

數值

  • 您無法變更數值欄位的 正負號 。
    • 和 int 之間的uint轉換無效。
  • 您可以變更數值欄位的 寬度 。
    • 例如,支持從 int 轉換成 long 或從 ulong 轉換成 ushort 。
    • 如果欄位的執行時值造成溢位,寬度縮小的轉換將引發例外狀況。
    • 只有在執行時間值小於 ulong時,才支援從 ushort 轉換成 ushort.MaxValue 。
    • 只有當執行時間值介於 double 和 float 之間時,才支援從 float.MinValue 轉換成 float.MaxValue。
    • 同樣地,對於 decimal,其範圍比 double 和 float都窄。

複印機

Orleans 預設提升安全性,包括防範某些並行錯誤類型的安全性。 尤其是,Orleans 預設會立即複製在計算單元呼叫中傳遞的物件。 Orleans.串行化有助於進行此複製。 當您套用 Orleans.CodeGeneration.GenerateSerializerAttribute 至類型時, Orleans 也會產生該類型的複製器。 Orleans 避免複製以標記 ImmutableAttribute的類型或個別成員。 如需詳細資訊,請參閱 中的Orleans不可變型別串行化。

串行化最佳做法

  • ✅ 請為您的類型使用[Alias("my-type")] 屬性來給予別名。 您可以重新命名具有別名的類型,而不會中斷相容性。

  • ❌ 請勿 將 變更 record 為一般 class ,反之亦然。 記錄和類別不會以相同方式表示,因為記錄除了一般成員之外,還有主要建構函式成員;因此,兩者無法互換。

  • ❌ 請勿 將新類型新增至可串行化類型的現有類型階層。 您不得將新的基類新增至現有的類型。 您可以安全地將新的子類別新增至現有的類型。

  • ✅ 請用SerializableAttribute取代GenerateSerializerAttribute的用法,並更換相應的IdAttribute宣告。

  • ✅ 請 針對每個類型將所有成員識別碼設定為零開始。 子類別及其基類中的標識碼可以安全地重疊。 下列範例中的這兩個屬性都有等於 0的標識碼。

    [GenerateSerializer]
    public class MyBaseClass
    {
        [Id(0)]
        public int MyBaseInt { get; set; }
    }
    
    [GenerateSerializer]
    public sealed class MySubClass : MyBaseClass
    {
        [Id(0)]
        public int MySubInt { get; set; }
    }
    
  • ✅ 請 視需要擴大數值成員類型。 您可以將 擴大 sbyte 為 shortint 至 long。

    • 您可以縮小數值成員類型,但如果觀察到的值無法由縮小類型正確表示,則會導致運行時間例外狀況。 例如, int.MaxValue 無法以 short 欄位表示,因此,如果遇到這類值,將字段縮小 int 為 short 可能會導致運行時間例外狀況。
  • ❌ 請勿 變更數值類型成員的符號屬性。 例如,您不得將成員的類型從 uint 變更為 int 或 int 變更為 uint。

穀物儲存串行化程式

Orleans 包含提供者支援的粒度持續性模型,可透過 State 屬性存取,或將一或多個 IPersistentState<TState> 值插入您的粒紋中。 在 7.0 之前 Orleans ,每個提供者都有不同的機制來設定串行化。 在 Orleans 7.0 中,現有提供一個通用的粒度狀態序列化介面 IGrainStorageSerializer,為每一個提供者的狀態序列化提供一致的自訂方式。 受支援的儲存提供者會依循某一模式,設定 IStorageProviderSerializerOptions.GrainStorageSerializer 提供者選項類別上的某項屬性,例如:

穀物儲存序列化目前預設為 Newtonsoft.Json 序列化狀態。 您可以在組態時間修改該屬性來取代這個屬性。 下列範例示範如何使用 OptionsBuilder<TOptions>:

public static void ConfigureGrainStorageSerializer(ISiloBuilder siloBuilder)
{
    siloBuilder.AddAzureBlobGrainStorage(
        "MyGrainStorage",
        (OptionsBuilder<AzureBlobStorageOptions> optionsBuilder) =>
        {
            optionsBuilder.Configure<MyCustomSerializer>(
                (options, serializer) => options.GrainStorageSerializer = serializer);
        });
}

如需詳細資訊,請參閱 OptionsBuilder API。

中 Orleans所使用的串行化大致有兩種:

  • 粒子呼叫序列化:用來序列化傳入和傳出粒子的物件。
  • 粒紋儲存串行化:用來將物件串行化至儲存系統及從儲存系統。

本文大部分著重於透過 中包含的 Orleans串行化架構進行粒紋呼叫串行化。 穀物儲存串行化程式一節討論糧食儲存串行化。

使用 Orleans 序列化

Orleans 包含一個被稱為Orleans.Serialization的進階且可擴充的序列化架構。 中包含的 Orleans 串行化架構是設計來符合下列目標:

  • 高效能:串行化程式已針對效能進行設計和優化。 此簡報提供更多詳細數據。
  • 高逼真度:串行化程式忠實地代表大部分 。NET 的類型系統,包括對泛型、多型、繼承階層、對象識別和循環圖形的支援。 不支援指標,因為它們無法跨進程移植。
  • 彈性:您可以透過建立替代或委派給外部序列化庫,例如System.Text.Json、Newtonsoft.Json 和 Google.Protobuf,來自定義序列化器,以支援第三方庫。
  • 版本容錯:串行化程式可讓應用程式類型隨著時間演進,支援:
    • 新增和移除成員
    • 子類別化
    • 數值擴大和縮小 (例如, int 從 到/從 long, float 到/從 double)
    • 重新命名類型

序列化器的高保真度型別表示頗不常見,因此有些點需要進一步說明:

  1. 動態類型和任意多型: Orleans 不會對傳入粒紋呼叫的類型強制執行限制,並維護實際數據類型的動態本質。 例如,如果粒紋介面中的方法宣告為接受 IDictionary,但在運行時間,傳送者會傳遞 SortedDictionary<TKey,TValue>,接收者確實會取得 SortedDictionary (即使 “static contract”/grain 介面未指定此行為)。

  2. 維護對象識別:如果在 Grain 呼叫的參數中多次傳遞相同的物件,或從參數中被多次間接指向,Orleans 則只會將它序列化一次。 在接收端,Orleans 能夠正確還原所有參考,以確保在反序列化之後,相同物件的兩個指標仍然指向相同的物件。 在如下情況中,保留物件識別很重要:想像一下,A 將具有 100 個專案的字典傳送至粒紋 B,而字典中的 10 個索引鍵指向 A 端的相同物件 obj。 如果不保留物件身份,B 會收到含有 100 個條目的字典,其中 10 個鍵指向 obj 的 10 個不同複製品。 在保留對象識別的情況下,B 端的字典看起來與 A 端完全相同,這 10 個索引鍵都指向單一物件 obj。 請注意,由於 .NET 中的預設字串哈希程式代碼實作是隨機化每個進程,因此字典和哈希集中的值順序可能不會保留。

為了支援版本容錯,串行化程式會要求您明確說明要串行化哪些類型和成員。 我們試圖使這盡可能無痛。 使用標記所有可串行化的型 Orleans.GenerateSerializerAttribute 別,以指示 Orleans 為您的類型產生串行化程序代碼。 完成此動作之後,您可以使用包含的程式代碼修正,將所需的 Orleans.IdAttribute 新增至類型上可串行化的成員,如下所示:

當包含的類型在其成員上不包含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)]。

序列化外部類型的代理

有時候,您可能需要在沒有完整控制權的粒紋之間傳遞類型。 在這些情況下,在應用程式碼中手動轉換至或從自定義型別可能不切實際。 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的欄位,而Sub : Base類別可以宣告具有相同識別碼的不同欄位。 0

數值

  • 您無法變更數值欄位的 正負號 。
    • 和 int 之間的uint轉換無效。
  • 您可以變更數值欄位的 寬度 。
    • 例如,支持從 int 轉換成 long 或從 ulong 轉換成 ushort 。
    • 如果欄位的執行時值造成溢位,寬度縮小的轉換將引發例外狀況。
    • 只有在執行時間值小於 ulong時,才支援從 ushort 轉換成 ushort.MaxValue 。
    • 只有當執行時間值介於 double 和 float 之間時,才支援從 float.MinValue 轉換成 float.MaxValue。
    • 同樣地,對於 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 為 shortint 至 long。

    • 您可以縮小數值成員類型,但如果觀察到的值無法由縮小類型正確表示,則會導致運行時間例外狀況。 例如, int.MaxValue 無法以 short 欄位表示,因此,如果遇到這類值,將字段縮小 int 為 short 可能會導致運行時間例外狀況。
  • ❌ 請勿 變更數值類型成員的符號屬性。 例如,您不得將成員的類型從 uint 變更為 int 或 int 變更為 uint。

穀物儲存串行化程式

Orleans 包含提供者支援的粒度持續性模型,可透過 State 屬性存取,或將一或多個 IPersistentState<TState> 值插入您的粒紋中。 在 7.0 之前 Orleans ,每個提供者都有不同的機制來設定串行化。 在 Orleans 7.0 中,現有提供一個通用的粒度狀態序列化介面 IGrainStorageSerializer,為每一個提供者的狀態序列化提供一致的自訂方式。 受支援的儲存提供者會依循某一模式,設定 IStorageProviderSerializerOptions.GrainStorageSerializer 提供者選項類別上的某項屬性,例如:

穀物儲存序列化目前預設為 Newtonsoft.Json 序列化狀態。 您可以在組態時間修改該屬性來取代這個屬性。 下列範例示範如何使用 OptionsBuilder<TOptions>:

siloBuilder.AddAzureBlobGrainStorage(
    "MyGrainStorage",
    (OptionsBuilder<AzureBlobStorageOptions> optionsBuilder) =>
    {
        optionsBuilder.Configure<IMySerializer>(
            (options, serializer) => options.GrainStorageSerializer = serializer);
    });

如需詳細資訊,請參閱 OptionsBuilder API。

Orleans 具有進階且可延伸的串行化架構。 Orleans 串行化傳入微粒要求和回應消息的資料類型,以及微粒持續性狀態物件。 在此架構中, Orleans 會自動產生這些數據類型的串行化程序代碼。 除了對已支援 .NET 序列化的型別生成更有效率的序列化/反序列化之外,Orleans 還嘗試為粒子介面中使用的非 .NET 序列化型別生成序列化程式。 架構也包含一組適用於常用類型的有效率內建串行化程式:清單、字典、字串、基本類型、陣列等。

'serializer 的 Orleans兩個重要功能會將其與其他許多第三方串行化架構分開:動態類型/任意多型和對象識別。

  1. 動態類型和任意多型: Orleans 不會對傳入粒紋呼叫的類型強制執行限制,並維護實際數據類型的動態本質。 例如,如果粒紋介面中的方法宣告為接受 IDictionary,但在運行時間,傳送者會傳遞 SortedDictionary<TKey,TValue>,接收者確實會取得 SortedDictionary (即使 “static contract”/grain 介面未指定此行為)。

  2. 維護對象識別:如果在 Grain 呼叫的參數中多次傳遞相同的物件,或從參數中被多次間接指向,Orleans 則只會將它序列化一次。 在接收端,Orleans 能夠正確還原所有參考,以確保在反序列化之後,相同物件的兩個指標仍然指向相同的物件。 在如下情況中,保留物件識別很重要:想像一下,A 將具有 100 個專案的字典傳送至粒紋 B,而字典中的 10 個索引鍵指向 A 端的相同物件 obj。 如果不保留物件身份,B 會收到含有 100 個條目的字典,其中 10 個鍵指向 obj 的 10 個不同複製品。 在保留對象識別的情況下,B 端的字典看起來與 A 端完全相同,這 10 個索引鍵都指向單一物件 obj。

標準 .NET 二進位序列化程式提供上述兩種行為,因此我們也很重視在 Orleans 支援此標準和熟悉的行為。

產生的序列化器

Orleans 會使用下列規則來決定要產生哪些串行化程式:

  1. 掃描所有參考核心 Orleans 程式庫的組件中的所有類型。
  2. 從這些組件中,為直接在 grain 介面方法簽名或狀態類別簽名中引用的類型,或任何標記為 SerializableAttribute 的類型生成序列化工具。
  3. 此外,粒紋介面或實作專案可以藉由新增 KnownTypeAttribute 或 KnownAssemblyAttribute 元件層級屬性,指向任意類型進行串行化產生。 這些告知程式碼產生器為元件內的特定類型或所有符合條件的類型生成序列化器。 如需元件層級屬性的詳細資訊,請參閱 在元件層級套用屬性。

備援序列化

Orleans 支援在執行時傳輸任意類型。 因此,內建程式代碼產生器無法判斷將事先傳輸的整個類型集。 此外,某些型別無法產生序列化器,因為它們無法存取(例如,private)或者有無法存取的欄位(例如,readonly)。 因此,需要對非預期的類型或無法事先生成序列器的類型進行即時序列化。 負責這些類型的串行化程序稱為 後援串行化程式。 Orleans 隨附兩個備用序列化程式:

使用 FallbackSerializationProvider (用戶端) 和 ClientConfiguration (尋址接收器) 上的 GlobalConfiguration 屬性來設定後援串行化程式。

// 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提供兩個主要用途:

  1. 作為執行時在微粒和客戶端之間傳輸資料的有線格式。
  2. 作為保存長期數據的儲存格式,以供稍後擷取。

所產生的 Orleans 串行化程序適合第一個用途,因為其彈性、效能和多功能性。 它們不太適合第二個用途,因為它們不是能夠處理不同版本。 建議針對持續性數據設定版本容錯串行化程式,例如 通訊協定緩衝區。 通訊協定緩衝區透過 Orleans.Serialization.ProtobufSerializerMicrosoft.OrleansOrleansGoogleUtils NuGet 套件獲得支援。 請遵循所選串行化程式的最佳做法,以確保版本容錯。 如上所述,使用 SerializationProviders 組態屬性來設定第三方串行化程式。