Dela via


Serialisering i Orleans

Det finns i stort sett två typer av serialisering som används i Orleans:

  • Kornanrops serialisering – används för att serialisera objekt som skickas till och från korn.
  • Serialisering av kornlagring – används för att serialisera objekt till och från lagringssystem.

Majoriteten av den här artikeln är avsedd för serialisering av kornanrop via serialiseringsramverket som ingår i Orleans. I avsnittet Grain Storage serializers beskrivs serialiseringen för kornlagring.

Använda Orleans serialisering

Orleans innehåller ett avancerat och utökningsbart serialiseringsramverk som kan kallas Orleans. Serialisering. Serialiseringsramverket som ingår i Orleans är utformat för att uppfylla följande mål:

  • Höga prestanda – serialiseraren är utformad och optimerad för prestanda. Mer information finns i den här presentationen.
  • High-fidelity – serialiseraren representerar troget majoriteten av . NET:s typsystem, inklusive stöd för generiska objekt, polymorfism, arvshierarkier, objektidentitet och cykliska grafer. Pekare stöds inte eftersom de inte är portabla mellan processer.
  • Flexibilitet – Serialiseraren kan anpassas för att stödja bibliotek från tredje part genom att skapa surrogater eller delegera till externa serialiseringsbibliotek som System.Text.Json, Newtonsoft.Json och Google.Protobuf.
  • Versionstolerans – Serialiseraren gör att programtyper kan utvecklas över tid, med stöd för:
    • Lägga till och ta bort medlemmar
    • Underklassning
    • Numerisk breddning och förträngning (t.ex. int till/från long, float till/från double)
    • Byta namn på typer

Hög återgivning av typer är ganska ovanligt för serialiserare, så vissa punkter motiverar ytterligare utarbetande:

  1. Dynamiska typer och godtycklig polymorfism: Orleans tillämpar inte begränsningar för de typer som kan skickas i kornanrop och upprätthåller den faktiska datatypens dynamiska karaktär. Det innebär till exempel att om metoden i korngränssnitten deklareras att acceptera IDictionary , men vid körning, skickar SortedDictionary<TKey,TValue>avsändaren , kommer mottagaren verkligen att få SortedDictionary (även om gränssnittet "static contract"/grain inte angav det här beteendet).

  2. Underhåll av objektidentitet: Om samma objekt skickas flera typer i argumenten för ett kornanrop eller indirekt pekas mer än en gång från argumenten Orleans , serialiseras det bara en gång. På mottagarsidan Orleans återställer alla referenser korrekt så att två pekare till samma objekt fortfarande pekar på samma objekt efter deserialiseringen. Objektidentitet är viktigt att bevara i scenarier som följande. Imagine grain A skickar en ordlista med 100 poster till korn B och 10 av nycklarna i ordlistan pekar på samma objekt, obj, på A:s sida. Utan att bevara objektidentiteten skulle B få en ordlista med 100 poster med de 10 nycklarna som pekar på 10 olika kloner av obj. Med objektidentitetsbevarad ser ordlistan på B:s sida exakt ut som på A:s sida med de 10 nycklarna som pekar på ett enda objekt obj. Observera att eftersom standardsträngens hashkodimplementeringar i .NET är randomiserade per process kanske inte ordningsföljden för värden i ordlistor och hashuppsättningar (till exempel) bevaras.

För att stödja versionstolerans kräver serialiseraren att utvecklare är explicita om vilka typer och medlemmar som serialiseras. Vi har försökt göra detta så smärtfritt som möjligt. Du måste markera alla serialiserbara typer med Orleans.GenerateSerializerAttribute för att instruera Orleans att generera serialiserarkod för din typ. När du har gjort detta kan du använda den inkluderade kodkorrigeringen för att lägga till de nödvändiga Orleans.IdAttribute till serialiserbara medlemmar på dina typer, vilket visas här:

An animated image of the available code fix being suggested and applied on the GenerateSerializerAttribute when the containing type doesn't contain IdAttribute's on its members.

Här är ett exempel på en serialiserbar typ i Orleans, som visar hur du använder attributen.

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

Orleans stöder arv och serialiserar de enskilda lagren i hierarkin separat, så att de kan ha distinkta medlems-ID:t.

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

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

I föregående kod bör du notera att både Publication och har medlemmar med [Id(0)] även om Book härleds från PublicationBook . Detta är den rekommenderade metoden eftersom Orleans medlemsidentifierare är begränsade till arvsnivån, inte typen som helhet. Medlemmar kan läggas till och tas bort från Publication och Book oberoende av varandra, men det går inte att infoga en ny basklass i hierarkin när programmet har distribuerats utan särskild hänsyn.

Orleans stöder även serialiseringstyper med internal, privateoch readonly medlemmar, till exempel i den här exempeltypen:

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

Som standard Orleans serialiserar du din typ genom att koda dess fullständiga namn. Du kan åsidosätta detta genom att lägga till en Orleans.AliasAttribute. Om du gör det kommer din typ att serialiseras med ett namn som är motståndskraftigt mot att byta namn på den underliggande klassen eller flytta den mellan sammansättningar. Typalias är globalt begränsade och du kan inte ha två alias med samma värde i ett program. För generiska typer måste aliasvärdet innehålla antalet generiska parametrar som föregås av en backtick, MyGenericType<T, U> till exempel kan ha aliaset [Alias("mytype`2")].

Serialiseringstyper record

Medlemmar som definieras i en posts primära konstruktor har implicita ID:er som standard. Med andra ord har Orleans stöd för serialiseringstyper record . Det innebär att du inte kan ändra parameterordningen för en redan distribuerad typ, eftersom det bryter kompatibiliteten med tidigare versioner av ditt program (i händelse av en löpande uppgradering) och med serialiserade instanser av den typen i lagring och strömmar. Medlemmar som definierats i en posttyps brödtext delar inte identiteter med de primära konstruktorparametrarna.

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

Om du inte vill att de primära konstruktorparametrarna ska inkluderas automatiskt som Serializable-fält kan du använda [GenerateSerializer(IncludePrimaryConstructorParameters = false)].

Surrogater för serialisering av sekundärtyper

Ibland kan du behöva skicka typer mellan korn som du inte har fullständig kontroll över. I dessa fall kan det vara opraktiskt att konvertera till och från någon anpassad typ i programkoden manuellt. Orleans erbjuder en lösning för dessa situationer i form av surrogattyper. Surrogater serialiseras i stället för sin måltyp och har funktioner att konvertera till och från måltypen. Tänk på följande exempel på en sekundär typ och en motsvarande surrogat och konverterare:

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

I koden ovan:

  • MyForeignLibraryValueType är en typ utanför din kontroll som definieras i ett bibliotek som förbrukar.
  • MyForeignLibraryValueTypeSurrogate är en surrogattyp som mappar till MyForeignLibraryValueType.
  • RegisterConverterAttribute Anger att MyForeignLibraryValueTypeSurrogateConverter fungerar som en konverterare att mappa till och från de två typerna. Klassen är en implementering av IConverter<TValue,TSurrogate> gränssnittet.

Orleans stöder serialisering av typer i typhierarkier (typer som härleds från andra typer). Om en sekundär typ kan visas i en typhierarki (till exempel som basklass för en av dina egna typer) måste du dessutom implementera Orleans.IPopulator<TValue,TSurrogate> gränssnittet. Ta följande som exempel:

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

Versionsregler

Versionstolerans stöds förutsatt att utvecklaren följer en uppsättning regler när de ändrar typer. Om utvecklaren är bekant med system som Google Protocol Buffers (Protobuf) är dessa regler bekanta.

Sammansatta typer (class & struct)

  • Arv stöds, men det går inte att ändra arvshierarkin för ett objekt. Basklassen för en klass kan inte läggas till, ändras till en annan klass eller tas bort.
  • Med undantag för vissa numeriska typer, som beskrivs i avsnittet Numeriska värden nedan, kan fälttyper inte ändras.
  • Fält kan läggas till eller tas bort när som helst i en arvshierarki.
  • Det går inte att ändra fält-ID:t.
  • Fält-ID:er måste vara unika för varje nivå i en typhierarki, men kan återanvändas mellan basklasser och underklasser. Klassen Base kan till exempel deklarera ett fält med ID 0 och ett annat fält kan deklareras med Sub : Base samma ID, 0.

Matematik

  • Det går inte att ändra signeringen av ett numeriskt fält.
    • Konverteringar mellan int & uint är ogiltiga.
  • Bredden på ett numeriskt fält kan ändras.
    • T.ex. stöds konverteringar från int till long eller ulong till ushort .
    • Konverteringar som begränsar bredden utlöser om körningsvärdet för ett fält skulle orsaka ett spill.
      • Konvertering från ulong till ushort stöds endast om värdet vid körning är mindre än ushort.MaxValue.
      • Konverteringar från double till float stöds endast om körningsvärdet är mellan float.MinValue och float.MaxValue.
      • På samma sätt för decimal, som har ett smalare intervall än både double och float.

Kopiatorer

Orleans främjar säkerheten som standard. Detta omfattar säkerhet från vissa klasser av samtidighetsbuggar. I synnerhet Orleans kopierar du omedelbart objekt som skickas i kornanrop som standard. Den här kopieringen underlättas av Orleans. Serialisering och när Orleans.CodeGeneration.GenerateSerializerAttribute tillämpas på en typ Orleans genererar också kopiatorer för den typen. Orleans kommer att undvika att kopiera typer eller enskilda medlemmar som har markerats med hjälp av ImmutableAttribute. Mer information finns i Serialisering av oföränderliga typer i Orleans.

Metodtips för serialisering

  • Ge dina typer alias med hjälp av attributet [Alias("my-type")] . Typer med alias kan byta namn utan att bryta kompatibiliteten.

  • Ändra inte till record en vanlig class eller vice versa. Poster och klasser representeras inte på ett identiskt sätt eftersom poster har primära konstruktormedlemmar utöver vanliga medlemmar och därför är de två inte utbytbara.

  • Lägg inte till nya typer i en befintlig typhierarki för en serialiserbar typ. Du får inte lägga till en ny basklass i en befintlig typ. Du kan på ett säkert sätt lägga till en ny underklass till en befintlig typ.

  • Ersätt användning av SerializableAttribute med GenerateSerializerAttribute och motsvarande IdAttribute deklarationer.

  • Starta alla medlems-ID:er på noll för varje typ. ID:t i en underklass och dess basklass kan säkert överlappa varandra. Båda egenskaperna i följande exempel har ID:er som är 0lika med .

    [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; }
    }
    
  • Gör bredare numeriska medlemstyper efter behov. Du kan utvidga sbyte till short till intlong.

    • Du kan begränsa numeriska medlemstyper, men det resulterar i ett körningsundatag om observerade värden inte kan representeras korrekt av den begränsade typen. Det kan till exempel int.MaxValue inte representeras av ett short fält, så om du begränsar ett int fält till short kan det resultera i ett körningsundatag om ett sådant värde påträffades.
  • Ändra inte signering av en medlem av numerisk typ. Du får inte ändra en medlemstyp från uint till int eller till int en uint, till exempel.

Serialiserare för kornlagring

Orleans innehåller en leverantörsbaserad beständighetsmodell för korn, som nås via State egenskapen eller genom att mata in ett eller flera IPersistentState<TState> värden i ditt korn. Före Orleans 7.0 hade varje provider en annan mekanism för att konfigurera serialisering. I Orleans 7.0 finns det nu ett serialiserargränssnitt för generell användningstillstånd, IGrainStorageSerializer, som erbjuder ett konsekvent sätt att anpassa tillståndsserialisering för varje provider. Lagringsproviders som stöds implementerar ett mönster som innebär att du ställer in IStorageProviderSerializerOptions.GrainStorageSerializer egenskapen på providerns alternativklass, till exempel:

Grain Storage-serialisering är för närvarande standard för Newtonsoft.Json serialisera tillstånd. Du kan ersätta detta genom att ändra den egenskapen vid konfigurationstillfället. Följande exempel visar detta med hjälp av OptionsBuilder<TOptions>:

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

Mer information finns i OptionsBuilder API.

Orleans har ett avancerat och utökningsbart serialiseringsramverk. Orleans serialiserar datatyper som skickas i korniga begärande- och svarsmeddelanden samt kornbeständiga tillståndsobjekt. Som en del av det här ramverket Orleans genererar automatiskt serialiseringskod för dessa datatyper. Förutom att generera en effektivare serialisering/deserialisering för typer som redan är . NET-serializable försöker Orleans också generera serialiserare för typer som används i korngränssnitt som inte är . NET-serializable. Ramverket innehåller också en uppsättning effektiva inbyggda serialiserare för ofta använda typer: listor, ordlistor, strängar, primitiver, matriser osv.

Två viktiga funktioner Orleansi serialiseraren skiljer den från många andra serialiseringsramverk från tredje part: dynamiska typer/godtycklig polymorfism och objektidentitet.

  1. Dynamiska typer och godtycklig polymorfism: Orleans tillämpar inte begränsningar för de typer som kan skickas i kornanrop och upprätthåller den faktiska datatypens dynamiska karaktär. Det innebär till exempel att om metoden i korngränssnitten deklareras att acceptera IDictionary , men vid körning, skickar SortedDictionary<TKey,TValue>avsändaren , kommer mottagaren verkligen att få SortedDictionary (även om gränssnittet "static contract"/grain inte angav det här beteendet).

  2. Underhåll av objektidentitet: Om samma objekt skickas flera typer i argumenten för ett kornanrop eller indirekt pekas mer än en gång från argumenten Orleans , serialiseras det bara en gång. På mottagarsidan Orleans återställer alla referenser korrekt så att två pekare till samma objekt fortfarande pekar på samma objekt efter deserialiseringen. Objektidentitet är viktigt att bevara i scenarier som följande. Imagine grain A skickar en ordlista med 100 poster till korn B och 10 av nycklarna i ordlistan pekar på samma objekt, obj, på A:s sida. Utan att bevara objektidentiteten skulle B få en ordlista med 100 poster med de 10 nycklarna som pekar på 10 olika kloner av obj. Med objektidentitetsbevarad ser ordlistan på B:s sida exakt ut på A:s sida med de 10 nycklarna som pekar på ett enda objekt obj.

Ovanstående två beteenden tillhandahålls av standard .NET binär serialiserare och det var därför viktigt för oss att stödja denna standard och välbekanta beteende i Orleans också.

Genererade serialiserare

Orleans använder följande regler för att bestämma vilka serialiserare som ska genereras. Reglerna är:

  1. Sök igenom alla typer i alla sammansättningar som refererar till kärnbiblioteket Orleans .
  2. Av dessa sammansättningar: generera serialiserare för typer som refereras direkt i grain interfaces-metodsignaturer eller tillståndsklasssignatur eller för någon typ som är markerad med SerializableAttribute.
  3. Dessutom kan ett projekt för korngränssnitt eller implementering peka på godtyckliga typer för serialiseringsgenerering genom att lägga till attribut KnownTypeAttribute på sammansättningsnivå KnownAssemblyAttribute för att instruera kodgeneratorn att generera serialiserare för specifika typer eller alla berättigade typer i en sammansättning. Mer information om attribut på sammansättningsnivå finns i Tillämpa attribut på sammansättningsnivå.

Återställningsserialisering

Orleans stöder överföring av godtyckliga typer vid körning och därför kan den inbyggda kodgeneratorn inte fastställa hela uppsättningen typer som kommer att överföras i förväg. Dessutom kan vissa typer inte ha serialiserare som genererats för dem eftersom de är otillgängliga (till exempel private) eller har otillgängliga fält (till exempel readonly). Därför finns det ett behov av just-in-time-serialisering av typer som var oväntade eller inte kunde ha serialiserare genererade i förväg. Serialiseraren som ansvarar för dessa typer kallas för återställningsserialiseraren. Orleans fartyg med två reserv serialiserare:

Återställningsserialiseraren FallbackSerializationProvider kan konfigureras med hjälp av egenskapen både ClientConfiguration på klienten och GlobalConfiguration på silon.

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

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

Alternativt kan reserv-serialiseringsprovidern anges i XML-konfigurationen:

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

BinaryFormatterSerializer är standardåterställnings serialiseraren.

Undantags serialisering

Undantag serialiseras med hjälp av återställningsserialiseraren. Med standardkonfigurationen BinaryFormatter är återställningsserialiseraren och därför måste mönstret ISerializable följas för att säkerställa korrekt serialisering av alla egenskaper i en undantagstyp.

Här är ett exempel på en undantagstyp med korrekt implementerad serialisering:

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

Metodtips för serialisering

Serialisering har två huvudsakliga syften i Orleans:

  1. Som ett trådformat för att överföra data mellan korn och klienter vid körning.
  2. Som ett lagringsformat för att spara långlivade data för senare hämtning.

Serialiserarna som genereras av Orleans är lämpliga för det första ändamålet på grund av deras flexibilitet, prestanda och mångsidighet. De är inte lika lämpliga för det andra ändamålet, eftersom de inte uttryckligen är versionstoleranta. Vi rekommenderar att användarna konfigurerar en versionstolerant serialiserare, till exempel protokollbuffertar för beständiga data. Protokollbuffertar stöds via Orleans.Serialization.ProtobufSerializer microsoft .Orleans.OrleansGoogleUtils NuGet-paket. Metodtipsen för den specifika serialiseraren bör användas för att säkerställa versionstolerans. Serialiserare från tredje part kan konfigureras med hjälp av konfigurationsegenskapen SerializationProviders enligt beskrivningen ovan.