Dela via


Serialiseringsanpassning i Orleans

En viktig aspekt av Orleans är dess stöd för anpassning av serialisering, vilket är processen att konvertera ett objekt eller en datastruktur till ett format som kan lagras eller överföras och rekonstrueras senare. På så sätt kan utvecklare styra hur data kodas och avkodas när de skickas mellan olika delar av systemet. Serialiseringsanpassning kan vara användbart för att optimera prestanda, samverkan och säkerhet.

Serialiseringsproviders

Orleans innehåller två serialiserarimplementeringar:

Information om hur du konfigurerar något av dessa paket finns i Serialiseringskonfiguration i Orleans.

Anpassad serialiserarimplementering

Det finns några vanliga steg för att skapa en anpassad serialiserarimplementering. Du måste implementera flera gränssnitt och sedan registrera serialiseraren med körningen Orleans . I följande avsnitt beskrivs stegen mer detaljerat.

Börja med att implementera följande Orleans serialiseringsgränssnitt:

  • IGeneralizedCodec: En codec som stöder flera typer.
  • IGeneralizedCopier: Tillhandahåller funktioner för att kopiera objekt av flera typer.
  • ITypeFilter: Funktioner för att tillåta att typer läses in och att delta i serialisering och deserialisering.

Tänk dig följande exempel på en anpassad serialiserarimplementering:

internal sealed class CustomOrleansSerializer :
    IGeneralizedCodec, IGeneralizedCopier, ITypeFilter
{
    void IFieldCodec.WriteField<TBufferWriter>(
        ref Writer<TBufferWriter> writer, 
        uint fieldIdDelta,
        Type expectedType,
        object value) =>
        throw new NotImplementedException();

    object IFieldCodec.ReadValue<TInput>(
        ref Reader<TInput> reader, Field field) =>
        throw new NotImplementedException();

    bool IGeneralizedCodec.IsSupportedType(Type type) =>
        throw new NotImplementedException();

    object IDeepCopier.DeepCopy(object input, CopyContext context) =>
        throw new NotImplementedException();

    bool IGeneralizedCopier.IsSupportedType(Type type) =>
        throw new NotImplementedException();
}

I föregående exempelimplementering:

  • Varje gränssnitt implementeras uttryckligen för att undvika konflikter med metodnamnsmatchning.
  • Varje metod genererar en NotImplementedException för att indikera att metoden inte har implementerats. Du måste implementera varje metod för att tillhandahålla önskade funktioner.

Nästa steg är att registrera serialiseraren med körningen Orleans . Detta uppnås vanligtvis genom att ISerializerBuilder utöka och exponera en anpassad AddCustomSerializer tilläggsmetod. I följande exempel visas det typiska mönstret:

using Microsoft.Extensions.DependencyInjection;
using Orleans.Serialization;
using Orleans.Serialization.Serializers;
using Orleans.Serialization.Cloning;

public static class SerializationHostingExtensions
{
    public static ISerializerBuilder AddCustomSerializer(
        this ISerializerBuilder builder)
    {
        var services = builder.Services;

        services.AddSingleton<CustomOrleansSerializer>();
        services.AddSingleton<IGeneralizedCodec, CustomOrleansSerializer>();
        services.AddSingleton<IGeneralizedCopier, CustomOrleansSerializer>();
        services.AddSingleton<ITypeFilter, CustomOrleansSerializer>();

        return builder;
    }
}

Ytterligare överväganden skulle vara att exponera en överlagring som accepterar anpassade serialiseringsalternativ som är specifika för din anpassade implementering. Dessa alternativ kan konfigureras tillsammans med registreringen i byggaren. Dessa alternativ kan vara beroende som matas in i din anpassade serialiserarimplementering.

Orleans stöder integrering med serialiserare från tredje part med hjälp av en providermodell. Detta kräver en implementering av den IExternalSerializer typ som beskrivs i avsnittet om anpassad serialisering i den här artikeln. Integreringar för vissa vanliga serialiserare underhålls tillsammans med Orleans, till exempel:

Anpassad implementering av IExternalSerializer beskrivs i följande avsnitt.

Anpassade externa serialiserare

Förutom automatisk serialiseringsgenerering kan appkod tillhandahålla anpassad serialisering för de typer som den väljer. Orleans rekommenderar att du använder den automatiska serialiseringsgenereringen för de flesta av dina apptyper och endast skriver anpassade serialiserare i sällsynta fall när du tror att det är möjligt att få bättre prestanda genom att handkoda serialiserare. Den här anteckningen beskriver hur du gör det och identifierar vissa specifika fall när det kan vara till hjälp.

Det finns tre sätt på vilka appar kan anpassa serialisering:

  1. Lägg till serialiseringsmetoder i din typ och markera dem med lämpliga attribut (CopierMethodAttribute, SerializerMethodAttribute, DeserializerMethodAttribute). Den här metoden är att föredra för typer som din app äger, dvs. de typer som du kan lägga till nya metoder i.
  2. Implementera IExternalSerializer och registrera den under konfigurationstiden. Den här metoden är användbar för att integrera ett externt serialiseringsbibliotek.
  3. Skriv en separat statisk klass som kommenterats med en [Serializer(typeof(YourType))] med de tre serialiseringsmetoderna i den och samma attribut som ovan. Den här metoden är användbar för typer som appen inte äger, till exempel typer som definierats i andra bibliotek som appen inte har någon kontroll över.

Var och en av dessa serialiseringsmetoder beskrivs i följande avsnitt.

Introduktion till anpassad serialisering

Orleans serialisering sker i tre steg:

  • Objekt kopieras omedelbart djupt för att säkerställa isolering.
  • Innan objekt sätts på tråden serialiseras de till en meddelandebyteström.
  • När objekten levereras till målaktiveringen återskapas de (deserialiseras) från den mottagna byteströmmen.

Datatyper som kan skickas i meddelanden, det vill: typer som kan skickas som metodargument eller returvärden, måste ha associerade rutiner som utför dessa tre steg. Vi refererar till dessa rutiner tillsammans som serialiserare för en datatyp.

Kopiatorn för en typ står ensam, medan serialiseraren och deserialiseraren är ett par som fungerar tillsammans. Du kan bara ange en anpassad kopiator, eller bara en anpassad serialiserare och en anpassad deserializer, eller så kan du tillhandahålla anpassade implementeringar av alla tre.

Serialiserare registreras för varje datatyp som stöds vid silostart och när en sammansättning läses in. Registrering är nödvändig för anpassade serialiserarrutiner för att en typ ska kunna användas. Serialiserarval baseras på den dynamiska typen av objekt som ska kopieras eller serialiseras. Därför behöver du inte skapa serialiserare för abstrakta klasser eller gränssnitt, eftersom de aldrig kommer att användas.

När du ska skriva en anpassad serialiserare

En handgjord serialiserarrutin presterar sällan bättre än de genererade versionerna. Om du är frestad att skriva en bör du först överväga följande alternativ:

  • Om det finns fält eller egenskaper i dina datatyper som inte behöver serialiseras eller kopieras kan du markera dem med NonSerializedAttribute. Detta gör att den genererade koden hoppar över dessa fält vid kopiering och serialisering. Använd ImmutableAttribute och Immutable<T> där det är möjligt för att undvika att kopiera oföränderliga data. Mer information finns i Optimera kopiering. Om du undviker att använda vanliga generiska samlingstyper ska du inte göra det. Körningen Orleans innehåller anpassade serialiserare för de generiska samlingar som använder semantiken i samlingarna för att optimera kopiering, serialisering och deserialisering. Dessa samlingar har också särskilda "förkortade" representationer i den serialiserade byteströmmen, vilket resulterar i ännu fler prestandafördelar. Till exempel kommer en Dictionary<string, string> att vara snabbare än en List<Tuple<string, string>>.

  • Det vanligaste fallet där en anpassad serialiserare kan ge en märkbar prestandavinst är när det finns betydande semantisk information kodad i datatypen som inte är tillgänglig genom att bara kopiera fältvärden. Matriser som är glest ifyllda kan till exempel ofta serialiseras mer effektivt genom att matrisen behandlas som en samling index/värde-par, även om appen behåller data som en fullständigt realiserad matris för driftshastighet.

  • En viktig sak att göra innan du skriver en anpassad serialiserare är att se till att den genererade serialiseraren skadar dina prestanda. Profilering hjälper lite här, men ännu mer värdefullt är att köra stresstester från slutpunkt till slutpunkt för din app med varierande serialiseringsbelastningar för att mäta effekten på systemnivå snarare än mikropåverkan av serialisering. Om du till exempel skapar en testversion som inte skickar några parametrar till eller resultat från kornmetoder, helt enkelt med hjälp av konserverade värden i båda änderna, kommer du att zooma in på effekten av serialisering och kopiering på systemets prestanda.

Lägga till serialiseringsmetoder till en typ

Alla serialiserarrutiner ska implementeras som statiska medlemmar i klassen eller structen som de arbetar med. Namnen som visas här är inte obligatoriska. registrering baseras på förekomsten av respektive attribut, inte på metodnamn. Observera att serialiserarmetoder inte behöver vara offentliga.

Om du inte implementerar alla tre serialiseringsrutinerna bör du markera din typ med SerializableAttribute så att de metoder som saknas genereras åt dig.

Kopiator

Kopieringsmetoder flaggas med Orleans.CodeGeneration.CopierMethodAttribute:

[CopierMethod]
static private object Copy(object input, ICopyContext context)
{
    // ...
}

Kopierare är vanligtvis de enklaste serialiserarrutinerna att skriva. De tar ett objekt som garanterat är av samma typ som den typ som kopiatorn definieras i och måste returnera en semantiskt likvärdig kopia av objektet.

Om ett underobjekt måste kopieras som en del av kopieringsobjektet är det bästa sättet att göra det att använda rutinen SerializationManager.DeepCopyInner :

var fooCopy = SerializationManager.DeepCopyInner(foo, context);

Viktigt!

Det är viktigt att använda SerializationManager.DeepCopyInner, i stället för SerializationManager.DeepCopy, för att underhålla objektidentitetskontexten för den fullständiga kopieringsåtgärden.

Underhålla objektidentitet

Ett viktigt ansvar för en kopieringsrutin är att underhålla objektidentiteten. Körningen Orleans tillhandahåller en hjälpklass för det här ändamålet. Innan du kopierar ett underobjekt "för hand" (inte genom att anropa DeepCopyInner), kontrollerar du om det redan har refererats till på följande sätt:

var fooCopy = context.CheckObjectWhileCopying(foo);
if (fooCopy is null)
{
    // Actually make a copy of foo
    context.RecordObject(foo, fooCopy);
}

Den sista raden är anropet till RecordObject, som krävs så att eventuella framtida referenser till samma objekt som foo referenser hittas korrekt av CheckObjectWhileCopying.

Kommentar

Detta bör endast göras för klassinstanser, intestruct instanser eller .NET-primitiver som string, Urioch enum.

Om du använder DeepCopyInner för att kopiera underobjekt hanteras objektidentitet åt dig.

Serialiserare

Serialiseringsmetoder flaggas med Orleans.CodeGeneration.SerializerMethodAttribute:

[SerializerMethod]
static private void Serialize(
    object input,
    ISerializationContext context,
    Type expected)
{
    // ...
}

Precis som med kopiatorer är "indata"-objektet som skickas till en serialiserare garanterat en instans av den definierande typen. Den förväntade typen kan ignoreras. Den baseras på information av typen kompileringstid om dataobjektet och används på en högre nivå för att bilda typprefixet i byteströmmen.

Om du vill serialisera underobjekt använder du rutinen SerializationManager.SerializeInner :

SerializationManager.SerializeInner(foo, context, typeof(FooType));

Om det inte finns någon viss förväntad typ för foo kan du skicka null för den förväntade typen.

Klassen BinaryTokenStreamWriter innehåller en mängd olika metoder för att skriva data till byteströmmen. En instans av klassen kan hämtas via egenskapen context.StreamWriter . Se klassen för dokumentation.

Deserializer

Deserialiseringsmetoder flaggas med Orleans.CodeGeneration.DeserializerMethodAttribute:

[DeserializerMethod]
static private object Deserialize(
    Type expected,
    IDeserializationContext context)
{
    //...
}

Den förväntade typen kan ignoreras. Den baseras på information av typen kompileringstid om dataobjektet och används på en högre nivå för att bilda typprefixet i byteströmmen. Den faktiska typen av objekt som ska skapas är alltid den typ av klass där deserialiseraren definieras.

Om du vill deserialisera underobjekt använder du rutinen SerializationManager.DeserializeInner :

var foo = SerializationManager.DeserializeInner(typeof(FooType), context);

Eller, alternativt:

var foo = SerializationManager.DeserializeInner<FooType>(context);

Om det inte finns någon viss förväntad typ för foo använder du den icke-generiska DeserializeInner varianten och skickar null för den förväntade typen.

Klassen BinaryTokenStreamReader innehåller en mängd olika metoder för att läsa data från byteströmmen. En instans av klassen kan hämtas via egenskapen context.StreamReader . Se klassen för dokumentation.

Skriva en serialiserarprovider

I den här metoden implementerar Orleans.Serialization.IExternalSerializer och lägger du till den i SerializationProviderOptions.SerializationProviders egenskapen både ClientConfiguration på klienten och GlobalConfiguration på silon. Information om konfiguration finns i Serialiseringsproviders.

Implementeringar av IExternalSerializer följer det mönster som tidigare beskrivits för serialisering med tillägg av en Initialize metod och en IsSupportedType metod som Orleans använder för att avgöra om serialiseraren stöder en viss typ. Det här är gränssnittsdefinitionen:

public interface IExternalSerializer
{
    /// <summary>
    /// Initializes the external serializer. Called once when the serialization manager creates
    /// an instance of this type
    /// </summary>
    void Initialize(Logger logger);

    /// <summary>
    /// Informs the serialization manager whether this serializer supports the type for serialization.
    /// </summary>
    /// <param name="itemType">The type of the item to be serialized</param>
    /// <returns>A value indicating whether the item can be serialized.</returns>
    bool IsSupportedType(Type itemType);

    /// <summary>
    /// Tries to create a copy of source.
    /// </summary>
    /// <param name="source">The item to create a copy of</param>
    /// <param name="context">The context in which the object is being copied.</param>
    /// <returns>The copy</returns>
    object DeepCopy(object source, ICopyContext context);

    /// <summary>
    /// Tries to serialize an item.
    /// </summary>
    /// <param name="item">The instance of the object being serialized</param>
    /// <param name="context">The context in which the object is being serialized.</param>
    /// <param name="expectedType">The type that the deserializer will expect</param>
    void Serialize(object item, ISerializationContext context, Type expectedType);

    /// <summary>
    /// Tries to deserialize an item.
    /// </summary>
    /// <param name="context">The context in which the object is being deserialized.</param>
    /// <param name="expectedType">The type that should be deserialized</param>
    /// <returns>The deserialized object</returns>
    object Deserialize(Type expectedType, IDeserializationContext context);
}

Skriva en serialiserare för en enskild typ

I den här metoden skriver du en ny klass som kommenterats med ett attribut [SerializerAttribute(typeof(TargetType))], där TargetType är den typ som serialiseras och implementerar de tre serialiseringsrutinerna. Reglerna för hur du skriver dessa rutiner är identiska med dem när du IExternalSerializerimplementerar . Orleans[SerializerAttribute(typeof(TargetType))] använder för att fastställa att den här klassen är en serialiserare för TargetType och det här attributet kan anges flera gånger i samma klass om den kan serialisera flera typer. Nedan visas ett exempel för en sådan klass:

public class User
{
    public User BestFriend { get; set; }
    public string NickName { get; set; }
    public int FavoriteNumber { get; set; }
    public DateTimeOffset BirthDate { get; set; }
}

[Orleans.CodeGeneration.SerializerAttribute(typeof(User))]
internal class UserSerializer
{
    [CopierMethod]
    public static object DeepCopier(
        object original, ICopyContext context)
    {
        var input = (User)original;
        var result = new User();

        // Record 'result' as a copy of 'input'. Doing this
        // immediately after construction allows for data
        // structures that have cyclic references or duplicate
        // references. For example, imagine that 'input.BestFriend'
        // is set to 'input'. In that case, failing to record
        // the copy before trying to copy the 'BestFriend' field
        // would result in infinite recursion.
        context.RecordCopy(original, result);

        // Deep-copy each of the fields.
        result.BestFriend =
            (User)context.SerializationManager.DeepCopy(input.BestFriend);

        // strings in .NET are immutable, so they can be shallow-copied.
        result.NickName = input.NickName;
        // ints are primitive value types, so they can be shallow-copied.
        result.FavoriteNumber = input.FavoriteNumber;
        result.BirthDate =
            (DateTimeOffset)context.SerializationManager.DeepCopy(input.BirthDate);

        return result;
    }

    [SerializerMethod]
    public static void Serializer(
        object untypedInput, ISerializationContext context, Type expected)
    {
        var input = (User) untypedInput;

        // Serialize each field.
        SerializationManager.SerializeInner(input.BestFriend, context);
        SerializationManager.SerializeInner(input.NickName, context);
        SerializationManager.SerializeInner(input.FavoriteNumber, context);
        SerializationManager.SerializeInner(input.BirthDate, context);
    }

    [DeserializerMethod]
    public static object Deserializer(
        Type expected, IDeserializationContext context)
    {
        var result = new User();

        // Record 'result' immediately after constructing it.
        // As with the deep copier, this
        // allows for cyclic references and de-duplication.
        context.RecordObject(result);

        // Deserialize each field in the order that they were serialized.
        result.BestFriend =
            SerializationManager.DeserializeInner<User>(context);
        result.NickName =
            SerializationManager.DeserializeInner<string>(context);
        result.FavoriteNumber =
            SerializationManager.DeserializeInner<int>(context);
        result.BirthDate =
            SerializationManager.DeserializeInner<DateTimeOffset>(context);

        return result;
    }
}

Serialisera generiska typer

Parametern TargetType[Serializer(typeof(TargetType))] för kan vara en öppen-generisk typ, till exempel MyGenericType<T>. I så fall måste serialiserarklassen ha samma generiska parametrar som måltypen. Orleans skapar en konkret version av serialiseraren vid körning för varje betongtyp MyGenericType<T> som serialiseras, till exempel en för var och en av MyGenericType<int> och MyGenericType<string>.

Tips för att skriva serialiserare och deserialiserare

Ofta är det enklaste sättet att skriva ett serialiserar-/deserialiserarpar att serialisera genom att konstruera en bytematris och skriva matrislängden till strömmen, följt av själva matrisen och sedan deserialisera genom att återställa processen. Om matrisen är fast längd kan du utelämna den från strömmen. Detta fungerar bra när du har en datatyp som du kan representera kompakt och som inte har underobjekt som kan dupliceras (så att du inte behöver oroa dig för objektidentitet).

En annan metod, som är den metod som körningen Orleans använder för samlingar som ordlistor, fungerar bra för klasser med betydande och komplex intern struktur: använd instansmetoder för att komma åt objektets semantiska innehåll, serialisera innehållet och deserialisera genom att ange semantiskt innehåll i stället för det komplexa interna tillståndet. I den här metoden skrivs inre objekt med SerializeInner och läse med DeserializeInner. I det här fallet är det vanligt att även skriva en anpassad kopiator.

Om du skriver en anpassad serialiserare och det slutar se ut som en sekvens med anrop till SerializeInner för varje fält i klassen behöver du ingen anpassad serialiserare för den klassen.

Se även