Sdílet prostřednictvím


Serializace v Orleans

Existují široce dva druhy serializace používané v Orleans:

  • Serializace zrnitého volání – slouží k serializaci objektů předávaných do a z zrn.
  • Serializace zrnitého úložiště – slouží k serializaci objektů do a ze systémů úložiště.

Většina tohoto článku je vyhrazena pro odstupňované volání serializace prostřednictvím rozhraní serializace zahrnuté v Orleans. Část serializátorů úložiště zrnitosti popisuje serializaci úložiště zrnitého úložiště.

Použití Orleans serializace

Orleans obsahuje pokročilou a rozšiřitelnou architekturu serializace, kterou lze označovat jako Orleans. Serializace. Architektura serializace, která Orleans je součástí, je navržena tak, aby splňovala následující cíle:

  • Vysoký výkon - Serializátor je navržen a optimalizován pro výkon. Další podrobnosti jsou k dispozici v této prezentaci.
  • Vysoká věrnost - Serializátor věrně představuje většinu . Systém typů NET, včetně podpory obecných typů, polymorfismu, hierarchií dědičnosti, identity objektů a cyklických grafů Ukazatele nejsou podporovány, protože nejsou přenositelné mezi procesy.
  • Flexibilita – Serializátor lze přizpůsobit tak, aby podporoval knihovny třetích stran vytvořením náhradních dotazů nebo delegováním do externích serializačních knihoven, jako je System.Text.Json, Newtonsoft.Json a Google.Protobuf.
  • Tolerance verzí – serializátor umožňuje, aby se typy aplikací v průběhu času vyvíjely a podporovaly:
    • Přidávání a odebírání členů
    • Podtříděné
    • Číselné rozšíření a zužování (např. int na/z long, float do/z double)
    • Přejmenování typů

Reprezentace typů s vysokou věrností je pro serializátory poměrně neobvyklá, takže některé body vyžadují další vychytávací:

  1. Dynamické typy a libovolný polymorfismus: Orleans nevynucuje omezení pro typy, které je možné předat ve voláních odstupňovaných dat a zachovat dynamickou povahu skutečného datového typu. To znamená, že pokud je například metoda v rozhraní grain deklarována tak, aby přijímala IDictionary , ale za běhu, odesílatel předá SortedDictionary<TKey,TValue>, příjemce skutečně získá SortedDictionary (i když "statické kontrakt" /grain rozhraní neurčil toto chování).

  2. Údržba identity objektu: Pokud je stejný objekt předán více typů v argumentech volání agregace nebo nepřímo odkazuje více než jednou z argumentů, Orleans serializuje ho pouze jednou. Na straně přijímače obnoví všechny odkazy správně, Orleans aby dva ukazatele na stejný objekt stále ukazují na stejný objekt i po deserializaci. Identita objektu je důležitá k zachování ve scénářích, jako je následující. Představte si, že A odesílá slovník s 100 položkami do agregace B a 10 klíčů ve slovníku odkazuje na stejný objekt, objna straně A. Bez zachování identity objektu by B obdržel slovník 100 položek s těmito 10 klíči ukazující na 10 různých klonů obj. Při zachování identity objektu vypadá slovník na straně B přesně stejně jako na straně A s těmito 10 klíči ukazující na jeden objekt obj. Všimněte si, že vzhledem k tomu, že výchozí implementace kódu hash řetězce v .NET jsou randomizovány pro jednotlivé procesy, nemusí být zachováno řazení hodnot ve slovníkech a sadách hodnot hash (například).

Aby bylo možné podporovat odolnost proti verzím, serializátor vyžaduje, aby vývojáři měli explicitní informace o tom, které typy a členy jsou serializovány. Snažili jsme se, aby to bylo co nejbolestnější. Všechny serializovatelné typy Orleans.GenerateSerializerAttribute musíte označit pokynem Orleans k vygenerování serializačního kódu pro váš typ. Jakmile to uděláte, můžete pomocí zahrnuté opravy kódu přidat požadované Orleans.IdAttribute členy serializovatelné na vašich typech, jak je znázorněno zde:

Animovaný obrázek navrhované a použité opravy dostupného kódu na GenerateSerializerAttribute, pokud obsahující typ neobsahuje idAttribute jeho členy.

Zde je příklad serializovatelného typu , Orleansdemonstrující, jak použít atributy.

[GenerateSerializer]
public class Employee
{
    [Id(0)]
    public string Name { get; set; }
}

Orleans podporuje dědičnost a serializuje jednotlivé vrstvy v hierarchii samostatně, což jim umožní mít jedinečné ID členů.

[GenerateSerializer]
public class Publication
{
    [Id(0)]
    public string Title { get; set; }
}

[GenerateSerializer]
public class Book : Publication
{
    [Id(0)]
    public string ISBN { get; set; }
}

V předchozím kódu si všimněte, že oba Publication a Book mají členy, i [Id(0)] když Book jsou odvozeny od Publication. Toto je doporučený postup, Orleans protože identifikátory členů jsou vymezeny na úroveň dědičnosti, nikoli na typ jako celek. Členy je možné přidávat a odebírat Publication nezávisle Book na sobě, ale po nasazení aplikace není možné do hierarchie vložit novou základní třídu.

Orleans také podporuje serializaci typů s internal, privatea readonly členy, například v tomto příkladu typ:

[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}";
}

Ve výchozím nastavení Orleans serializuje váš typ kódováním jeho celého názvu. Můžete to přepsat přidáním .Orleans.AliasAttribute Výsledkem bude serializace typu pomocí názvu, který je odolný proti přejmenování základní třídy nebo jeho přesunutí mezi sestaveními. Aliasy typu jsou globálně vymezeny a v aplikaci nemůžete mít dva aliasy se stejnou hodnotou. U obecných typů musí hodnota aliasu obsahovat počet obecných parametrů, před kterými předchází zpětný znak, například MyGenericType<T, U> alias [Alias("mytype`2")].

Serializace record typů

Členové definované v primárním konstruktoru záznamu mají implicitní ID ve výchozím nastavení. Jinými slovy, Orleans podporuje serializaci record typů. To znamená, že nemůžete změnit pořadí parametrů pro již nasazený typ, protože tím dojde k narušení kompatibility s předchozími verzemi aplikace (v případě postupného upgradu) a serializovanými instancemi tohoto typu v úložišti a datových proudech. Členové definované v těle typu záznamu nesdílí identity s primárními parametry konstruktoru.

[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; }
}

Pokud nechcete, aby parametry primárního konstruktoru byly automaticky zahrnuty jako serializovatelná pole, můžete použít [GenerateSerializer(IncludePrimaryConstructorParameters = false)].

Náhradník pro serializaci cizích typů

Někdy může být potřeba předat typy mezi zrny, které nemáte plnou kontrolu nad. V těchto případech může být nepraktické převést na a z některého vlastního definovaného typu v kódu aplikace ručně. Orleans nabízí řešení pro tyto situace ve formě náhradních typů. Náhradní náhražky jsou serializovány místo jejich cílového typu a mají funkce pro převod na cílový typ a z cílového typu. Představte si následující příklad cizího typu a odpovídajícího náhradního a převaděče:

// 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
        };
}

V předchozím kódu:

  • Jedná se MyForeignLibraryValueType o typ mimo váš ovládací prvek definovaný v uživatelské knihovně.
  • Jedná se MyForeignLibraryValueTypeSurrogate o náhradní typ, který se mapuje na MyForeignLibraryValueType.
  • Určuje RegisterConverterAttribute , že MyForeignLibraryValueTypeSurrogateConverter funguje jako převaděč pro mapování na a z těchto dvou typů. Třída je implementace IConverter<TValue,TSurrogate> rozhraní.

Orleans podporuje serializaci typů v hierarchiích typů (typy odvozené od jiných typů). V případě, že se cizí typ může objevit v hierarchii typů (například jako základní třída pro jeden z vašich vlastních typů), musíte navíc implementovat Orleans.IPopulator<TValue,TSurrogate> rozhraní. Představte si následující příklad:

// 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; }
}

Pravidla správy verzí

Tolerance verzí je podporována, pokud vývojář při úpravách typů dodržuje sadu pravidel. Pokud je vývojář obeznámen se systémy, jako jsou vyrovnávací paměti protokolu Google (Protobuf), budou tato pravidla známá.

Složené typy (class & struct)

  • Dědičnost se podporuje, ale změna hierarchie dědičnosti objektu není podporována. Základní třídu třídy nelze přidat, změnit na jinou třídu nebo ji odebrat.
  • S výjimkou některých číselných typů, které jsou popsány v části Číselné hodnoty níže, nelze změnit typy polí.
  • Pole je možné přidat nebo odebrat v libovolném okamžiku v hierarchii dědičnosti.
  • ID polí nelze změnit.
  • ID polí musí být jedinečné pro každou úroveň v hierarchii typů, ale je možné je znovu použít mezi základními třídami a podtřídami. Třída může například Base deklarovat pole s ID 0 a jiné pole lze deklarovat pomocí Sub : Base stejného ID, 0.

Numerické výpočty

  • Podpis číselného pole nelze změnit.
    • Převody mezi int & uint jsou neplatné.
  • Šířku číselného pole je možné změnit.
    • Příklad: převody z int do long nebo ulong na ushort jsou podporovány.
    • Převody, které zúží šířku, vyvolá, pokud by hodnota modulu runtime pole způsobila přetečení.
      • Převod z ulong na ushort jsou podporovány pouze v případě, že hodnota za běhu je menší než ushort.MaxValue.
      • Převody z double na float jsou podporovány pouze v případě, že je hodnota modulu runtime mezi float.MinValue a float.MaxValue.
      • Podobně pro decimal, který má užší rozsah než oba double a float.

Kopírky

Orleans podporuje bezpečnost ve výchozím nastavení. To zahrnuje bezpečnost některých tříd chyb souběžnosti. Konkrétně Orleans se ve výchozím nastavení okamžitě zkopírují objekty předané ve voláních agregačních intervalů. Toto kopírování je usnadněno Orleans. Serializace a kdy Orleans.CodeGeneration.GenerateSerializerAttribute se použije u typu, Orleans vygeneruje pro tento typ také kopírovací moduly. Orleans zabrání kopírování typů nebo jednotlivých členů, které jsou označeny pomocí ImmutableAttribute. Další podrobnosti naleznete v tématu Serializace neměnných typů v Orleans.

Osvědčené postupy serializace

  • Udělte svým typům aliasy pomocí atributu [Alias("my-type")] . Typy s aliasy je možné přejmenovat bez narušení kompatibility.

  • Neměňte record běžnou class nebo naopak. Záznamy a třídy nejsou reprezentovány stejným způsobem, protože záznamy mají kromě běžných členů kromě běžných členů primární konstruktor, a proto nejsou vzájemně zaměnitelné.

  • Nepřidávejte nové typy do existující hierarchie typů pro serializovatelný typ. Do existujícího typu nesmíte přidat novou základní třídu. Do existujícího typu můžete bezpečně přidat novou podtřídu.

  • Nahraďte použití SerializableAttribute odpovídajícími IdAttribute deklaracemiGenerateSerializerAttribute.

  • Pro každý typ zahajte všechna ID členů na nule. ID v podtřídě a její základní třídě se můžou bezpečně překrývat. Obě vlastnosti v následujícím příkladu mají ID rovna 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; }
    }
    
  • Podle potřeby rozšiřte číselné typy členů. Můžete rozšířit sbyte na short int long.

    • Číselné typy členů můžete zúžit, ale v případě, že pozorované hodnoty nemohou být správně reprezentovány zúženým typem, dojde k výjimce za běhu. Například int.MaxValue pole nelze reprezentovat short , takže zúžení int pole na může vést k short výjimce za běhu, pokud byla taková hodnota zjištěna.
  • Neměňte signičnost člena číselného typu. Nelze změnit typ člena z uint na int nebo int na typ uint, například.

Serializátory úložiště zrnitosti

Orleans zahrnuje model trvalosti založené na poskytovateli pro zrnka, který je přístupný prostřednictvím State vlastnosti, nebo vložením jedné nebo více IPersistentState<TState> hodnot do agregace. Před Orleans verzí 7.0 měl každý poskytovatel jiný mechanismus konfigurace serializace. Ve Orleans verzi 7.0 je nyní rozhraní serializátoru stavu pro obecné účely, IGrainStorageSerializerkteré nabízí konzistentní způsob přizpůsobení serializace stavu pro každého zprostředkovatele. Podporovaní poskytovatelé úložiště implementují vzor, který zahrnuje nastavení IStorageProviderSerializerOptions.GrainStorageSerializer vlastnosti ve třídě možností poskytovatele, například:

Serializace úložiště agregačních hodnot se v současné době standardně Newtonsoft.Json serializuje do stavu serializace. Tuto vlastnost můžete nahradit úpravou této vlastnosti v době konfigurace. Následující příklad ukazuje použití OptionsBuilder<TOptions>:

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

Další informace naleznete v tématu OptionsBuilder API.

Orleans má pokročilou a rozšiřitelnou architekturu serializace. Orleans serializuje datové typy předané v odstupňované žádosti a odpovědi zprávy, stejně jako obilné trvalé stav objekty. V rámci této architektury Orleans automaticky generuje kód serializace pro tyto datové typy. Kromě generování efektivnější serializace /deserializace pro typy, které již jsou . NET-serializovatelný, Orleans také se snaží generovat serializátory pro typy používané v rozhraních zrn, které nejsou . NET serializovatelné. Tato architektura obsahuje také sadu efektivních předdefinovaných serializátorů pro často používané typy: seznamy, slovníky, řetězce, primitivy, pole atd.

Dvě důležité funkce serializátoru Orleansho nastavily kromě mnoha dalších architektur serializace třetích stran: dynamické typy/libovolná polymorfismus a identita objektu.

  1. Dynamické typy a libovolný polymorfismus: Orleans nevynucuje omezení pro typy, které je možné předat ve voláních odstupňovaných dat a zachovat dynamickou povahu skutečného datového typu. To znamená, že pokud je například metoda v rozhraní grain deklarována tak, aby přijímala IDictionary , ale za běhu, odesílatel předá SortedDictionary<TKey,TValue>, příjemce skutečně získá SortedDictionary (i když "statické kontrakt" /grain rozhraní neurčil toto chování).

  2. Údržba identity objektu: Pokud je stejný objekt předán více typů v argumentech volání agregace nebo nepřímo odkazuje více než jednou z argumentů, Orleans serializuje ho pouze jednou. Na straně přijímače obnoví všechny odkazy správně, Orleans aby dva ukazatele na stejný objekt stále ukazují na stejný objekt i po deserializaci. Identita objektu je důležitá k zachování ve scénářích, jako je následující. Představte si, že A odesílá slovník s 100 položkami do agregace B a 10 klíčů ve slovníku odkazuje na stejný objekt, obj, na straně A. Bez zachování identity objektu by B obdržel slovník 100 položek s těmito 10 klíči ukazující na 10 různých klonů obj. Když je identita objektu zachovaná, slovník na straně B vypadá přesně stejně jako na straně A s těmito 10 klíči ukazující na jeden objekt obj.

Výše uvedené dvě chování jsou poskytována standardním binárním serializátorem .NET a proto bylo pro nás důležité podporovat i toto standardní a známé chování Orleans .

Generované serializátory

Orleans používá následující pravidla k rozhodnutí, které serializátory se mají generovat. Pravidla jsou:

  1. Prohledejte všechny typy ve všech sestaveních, která odkazují na základní Orleans knihovnu.
  2. Z těchto sestavení: vygenerujte serializátory pro typy, které jsou přímo odkazovány v podpisech metody grain rozhraní nebo podpis třídy stavu nebo pro jakýkoli typ, který je označen SerializableAttribute.
  3. Kromě toho může rozhraní zrnitosti nebo projekt implementace odkazovat na libovolné typy pro generování serializace přidáním KnownTypeAttribute atributů nebo KnownAssemblyAttribute atributů na úrovni sestavení, které generátoru kódu můžou generovat serializátory pro konkrétní typy nebo všechny způsobilé typy v rámci sestavení. Další informace o atributech na úrovni sestavení naleznete v tématu Použití atributů na úrovni sestavení.

Náhradní serializace

Orleans podporuje přenos libovolných typů za běhu, a proto integrovaný generátor kódu nemůže určit celou sadu typů, které se budou přenášet předem. Kromě toho některé typy nemohou mít serializátory vygenerované pro ně, protože jsou nepřístupné (například private) nebo mají nepřístupná pole (například readonly). Proto je potřeba serializace typů, které byly neočekávané nebo nemohly mít serializátory vygenerované předem. Serializátor zodpovědný za tyto typy se nazývá náhradní serializátor. Orleans dodává se dvěma náhradními serializátory:

Náhradní serializátor lze nakonfigurovat pomocí FallbackSerializationProvider vlastnosti v ClientConfiguration klientovi i GlobalConfiguration na sila.

// Client configuration
var clientConfiguration = new ClientConfiguration();
clientConfiguration.FallbackSerializationProvider =
    typeof(FantasticSerializer).GetTypeInfo();

// Global configuration
var globalConfiguration = new GlobalConfiguration();
globalConfiguration.FallbackSerializationProvider =
    typeof(FantasticSerializer).GetTypeInfo();

Alternativně lze v konfiguraci XML zadat záložního zprostředkovatele serializace:

<Messaging>
    <FallbackSerializationProvider
        Type="GreatCompany.FantasticFallbackSerializer, GreatCompany.SerializerAssembly"/>
</Messaging>

Jedná se BinaryFormatterSerializer o výchozí záložní serializátor.

Upozorňující

Binární serializace s BinaryFormatter může být nebezpečná. Další informace najdete v průvodci zabezpečením BinaryFormatter a průvodce migrací BinaryFormatter.

Serializace výjimek

Výjimky jsou serializovány pomocí náhradní serializátoru. Při použití výchozí konfigurace BinaryFormatter je záložní serializátor, a proto musí být dodržen vzor ISerializable, aby bylo zajištěno správné serializace všech vlastností v typu výjimky.

Tady je příklad typu výjimky s správně implementovanou serializací:

[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);
    }
}

Osvědčené postupy serializace

Serializace slouží dvěma primárním účelům v Orleans:

  1. Jako formát drátu pro přenos dat mezi zrnky a klienty za běhu.
  2. Jako formát úložiště pro uchování dlouhodobých dat pro pozdější načtení.

Serializátory generované Orleans jsou vhodné pro první účel z důvodu jejich flexibility, výkonu a všestrannosti. Nejsou tak vhodné pro druhý účel, protože nejsou explicitně odolné vůči verzím. Doporučuje se, aby uživatelé nakonfigurovali serializátor odolný proti verzím, jako jsou vyrovnávací paměti protokolu pro trvalá data. Vyrovnávací paměti protokolu jsou podporovány prostřednictvím Orleans.Serialization.ProtobufSerializer microsoft.Orleans. Balíček NuGet OrleansGoogleUtils. Osvědčené postupy pro konkrétní serializátor podle výběru by se měly použít k zajištění tolerance verzí. Serializátory třetích stran je možné nakonfigurovat pomocí SerializationProviders vlastnosti konfigurace, jak je popsáno výše.