Freigeben über


Serialisierung in Orleans

Es gibt im Allgemeinen zwei Arten der Serialisierung, die in Orleans verwendet werden:

  • Serialisierung von Grainaufrufen: Dient zum Serialisieren von Objekten, die an und aus Grains übergeben werden.
  • Serialisierung des Grainspeichers: Wird zum Serialisieren von Objekten in und aus Speichersystemen verwendet.

Der Großteil dieses Artikels widmet sich der Serialisierung von Grainaufrufen über das in Orleans enthaltene Serialisierungsframework. Im Abschnitt Grainspeicherserialisierung wird die Serialisierung des Grain-Speichers erläutert.

Verwenden von Orleans-Serialisierung

Orleans enthält ein fortschrittliches und erweiterbares Serialisierungsframework, das als Orleans.Serialization bezeichnet werden kann. Das in Orleans enthaltene Serialisierungsframework ist so konzipiert, dass es die folgenden Ziele erfüllt:

  • Hohe Leistung: Die Serialisierung wurde im Hinblick auf Leistung entwickelt und optimiert. Weitere Informationen finden Sie in dieser Präsentation.
  • Hohe Genauigkeit: Die Serialisierung stellt die Mehrheit des .NET-Typsystems korrekt dar, einschließlich Unterstützung für Generics, Polymorphismus, Vererbungshierarchien, Objektidentität und zyklische Diagramme. Zeiger werden nicht unterstützt, da sie nicht prozessübergreifend portierbar sind.
  • Flexibilität: Das Serialisierungsprogramm kann angepasst werden, um Bibliotheken von Drittanbietern zu unterstützen, indem Ersatzzeichen erstellt oder an externe Serialisierungsbibliotheken wie System.Text.Json, Newtonsoft.Json und Google.Protobuf delegiert werden.
  • Versionstoleranz: Die Serialisierung ermöglicht die Weiterentwicklung von Anwendungstypen im Laufe der Zeit und unterstützt Folgendes:
    • Hinzufügen und Entfernen von Membern
    • Unterklassen
    • Numerische Verbreiterung und Verengung (z. B int . zu/von long, float zu/von double)
    • Umbenennung von Typen

Die korrekte Darstellung von Typen ist für Serialisierungsmodule ziemlich ungewöhnlich, daher verdienen einige Punkte eine detailliertere Betrachtung:

  1. Dynamische Typen und beliebige Polymorphie:Orleanserzwingt keine Beschränkungen für die Typen, die in Grainaufrufen übergeben werden können, und behält die dynamische Natur des tatsächlichen Datentyps bei. Das bedeutet z. B., dass, wenn die Methode in den Grainschnittstellen so deklariert ist, dass sie IDictionary akzeptiert, der Sender aber zur Runtime SortedDictionary<TKey,TValue> übergibt, der Empfänger tatsächlich SortedDictionary erhält (obwohl der „statische Vertrag“ bzw. die Grainschnittstelle dieses Verhalten nicht festgelegt hat).

  2. Beibehalten der Objektidentität: Wenn dasselbe Objekt in den Argumenten eines Grainaufrufs mehrfach übergeben wird oder die Argumente mehr als einmal indirekt darauf verweisen, wird es von Orleans nur einmal serialisiert. Auf der Empfängerseite wird Orleans alle Verweise ordnungsgemäß wiederherstellen, sodass zwei Zeiger auf dasselbe Objekt auch nach der Deserialisierung noch auf dasselbe Objekt zeigen. In Szenarien wie dem folgenden ist es wichtig, die Objektidentität zu wahren. Stellen Sie sich vor, Grain A sendet ein Wörterbuch mit 100 Einträgen an Grain B, und 10 der Schlüssel im Wörterbuch verweisen auf dasselbe Objekt obj auf der Seite von A. Ohne Beibehaltung der Objektidentität würde B ein Wörterbuch mit 100 Einträgen erhalten, von denen diese 10 Schlüssel auf 10 verschiedene Klone von obj verweisen. Bei beibehaltener Objektidentität sieht das Wörterbuch auf der Seite von B genau so wie auf der Seite von A aus, und diese 10 Schlüssel verweisen auf ein einzelnes Objekt obj. Beachten Sie, dass die Reihenfolge von Werten in Wörterbüchern und (beispielsweise) Hashsätzen möglicherweise nicht beibehalten wird, da die standardmäßigen Zeichenfolgenhash-Codeimplementierungen in .NET prozessweise randomisiert werden.

Damit die Versionstoleranz unterstützt werden kann, erfordert das Serialisierungsmodul, dass Entwickler explizit angeben, welche Typen und Member serialisiert werden. Wir haben versucht, dies so schmerzfrei wie möglich zu gestalten. Sie müssen alle serialisierbaren Typen mit Orleans.GenerateSerializerAttribute markieren, um Orleans anzuweisen, Serialisierungsmodulcode für Ihren Typ zu generieren. Im Anschluss daran können Sie die dazugehörige Codekorrektur verwenden, um das erforderliche Orleans.IdAttribute-Element den serialisierbaren Elementen Ihrer Typen hinzuzufügen, wie hier gezeigt:

Ein animiertes Bild der verfügbaren Codekorrektur, die vorgeschlagen und auf GenerateSerializerAttribute angewendet wird, wenn der enthaltende Typ keine IdAttribute-Instanzen für seine Member enthält.

Hier sehen Sie ein Beispiel für einen serialisierbaren Typ in Orleans, der veranschaulicht, wie die Attribute angewendet werden.

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

Orleans unterstützt Vererbung und serialisiert die einzelnen Ebenen in der Hierarchie separat, sodass sie unterschiedliche Member-IDs haben können.

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

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

Beachten Sie im vorherigen Code, dass sowohl Publication als auch Book Member mit [Id(0)] enthalten, obwohl Book von Publication abgeleitet ist. Dies ist die empfohlene Vorgehensweise in Orleans, da der Geltungsbereich von Memberbezeichnern sich auf die Vererbungsebene und nicht auf den Typ als Ganzes bezieht. Member können unabhängig von Publication und Book hinzugefügt und entfernt werden, es kann aber keine neue Basisklasse in die Hierarchie eingefügt werden, nachdem die Anwendung ohne besondere Berücksichtigung bereitgestellt wurde.

Orleans unterstützt auch serialisierende Typen mit internal-, private- und readonly-Membern, z. B. in diesem Beispieltyp:

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

Standardmäßig serialisiert Orleans Ihren Typ, indem dessen vollständiger Name codiert wird. Sie können dies außer Kraft setzen, indem Sie Orleans.AliasAttribute hinzufügen. Dies führt dazu, dass Ihr Typ mit einem Namen serialisiert wird, der gegenüber Umbenennung der zugrunde liegenden Klasse oder Verschieben zwischen Assemblys unempfindlich ist. Aliase von Typen gelten global, weshalb in einer Anwendung nicht zwei Aliase mit demselben Wert möglich sind. Bei generischen Typen muss der Aliaswert die Anzahl generischer Parameter enthalten, denen ein Graviszeichen vorangestellt ist, MyGenericType<T, U> kann z. B. den Alias [Alias("mytype`2")]haben.

Serialisieren von record-Typen

Im primären Konstruktor eines Datensatzes definierte Member haben standardmäßig implizite IDs. Anders ausgedrückt: Orleans unterstützt die Serialisierung von record-Typen. Das bedeutet, dass Sie die Parameterreihenfolge für einen bereits bereitgestellten Typ nicht ändern können, da dadurch keine Kompatibilität mit früheren Versionen Ihrer Anwendung (im Falle eines parallelen Upgrades) und mit serialisierten Instanzen dieses Typs in Speicher und Streams gewährleistet ist. Im Körper eines Datensatztyps definierte Member geben Identitäten nicht für die primären Konstruktorparameter frei.

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

Wenn die primären Konstruktorparameter nicht automatisch als serialisierbare Felder eingeschlossen werden sollen, können Sie [GenerateSerializer(IncludePrimaryConstructorParameters = false)]verwenden.

Ersatztypen für die Serialisierung fremder Typen

Mitunter müssen Sie Typen zwischen Grains übergeben, über die Sie keine volle Kontrolle haben. In diesen Fällen kann es unpraktisch sein, einen benutzerdefinierten Typ in Ihrem Anwendungscode manuell zu konvertieren. Orleans bietet eine Lösung für diese Situationen in Form von Ersatztypen. Ersatztypen werden anstelle ihres Zieltyps serialisiert und verfügen über Funktionen zum Konvertieren in und aus dem Zieltyp. Betrachten Sie das folgende Beispiel eines fremden Typs und eines entsprechenden Ersatztyps und Konverters:

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

Für den Code oben gilt:

  • MyForeignLibraryValueType ist ein Typ außerhalb Ihrer Kontrolle, der in einer nutzenden Bibliothek definiert ist.
  • MyForeignLibraryValueTypeSurrogate ist ein Ersatztyp, der MyForeignLibraryValueType zugeordnet ist.
  • RegisterConverterAttribute gibt an, dass MyForeignLibraryValueTypeSurrogateConverter als Konverter fungiert, um die beiden Typen einander zuzuordnen. Diese Klasse ist eine Implementierung der IConverter<TValue,TSurrogate>-Schnittstelle.

Orleans unterstützt die Serialisierung von Typen in Typhierarchien (Typen, die von anderen Typen abgeleitet sind). Für den Fall, dass ein fremder Typ in einer Typhierarchie auftaucht (z. B. als Basisklasse für einen Ihrer eigenen Typen), müssen Sie die Orleans.IPopulator<TValue,TSurrogate>-Schnittstelle zusätzlich implementieren. Betrachten Sie das folgenden Beispiel:

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

Regeln zur Versionsverwaltung

Die Versionstoleranz wird unter der Voraussetzung unterstützt, dass der Entwickler beim Ändern von Typen eine Reihe von Regeln befolgt. Wenn der Entwickler mit Systemen wie Google Protocol Buffers (Protobuf) vertraut ist, sind ihm diese Regeln bekannt.

Zusammengesetzte Typen (class & struct)

  • Vererbung wird unterstützt, aber das Ändern der Vererbungshierarchie eines Objekts wird nicht unterstützt. Die Basisklasse einer Klasse kann nicht hinzugefügt, in eine andere Klasse geändert oder entfernt werden.
  • Mit Ausnahme einiger numerischer Typen, die unten im Abschnitt Numerische Werte beschrieben werden, können Feldtypen nicht geändert werden.
  • Felder können an jedem beliebigen Punkt in einer Vererbungshierarchie hinzugefügt oder entfernt werden.
  • Feld-IDs können nicht geändert werden.
  • Feld-IDs müssen für jede Ebene in einer Typhierarchie eindeutig sein, können aber zwischen Basisklassen und Unterklassen wiederverwendet werden. Beispielsweise kann die Base-Klasse ein Feld mit der ID 0 deklarieren, und ein anderes Feld mit der gleichen ID 0 kann von Sub : Base deklariert werden.

Numerische Werte (Numerics)

  • Die Vorzeichenbehaftung eines numerischen Felds kann nicht geändert werden.
    • Konvertierungen zwischen int & uint sind ungültig.
  • Die Breite eines numerischen Felds kann geändert werden.
    • Beispiel: Konvertierungen von int zu long oder ulong zu ushort werden unterstützt.
    • Konvertierungen, die die Breite einschränken, lösen Ausnahmen aus, wenn der Laufzeitwert eines Felds einen Überlauf verursachen würde.
      • Konvertierungen von ulong in ushort werden nur unterstützt, wenn der Wert zur Laufzeit kleiner als ushort.MaxValue ist.
      • Konvertierungen von double in float werden nur unterstützt, wenn der Laufzeitwert zwischen float.MinValue und float.MaxValue liegt.
      • Ebenso für decimal, das einen engeren Bereich als double und float aufweist.

Kopierer

Orleans fördert standardmäßig die Sicherheit. Dies schließt die Sicherheit vor einigen Klassen von Parallelitätsfehlern ein. Insbesondere kopiert Orleans Objekte, die in Grain-Aufrufen übergeben werden, standardmäßig sofort. Dieses Kopieren wird durch Orleans.Serialization unterstützt, und wenn Orleans.CodeGeneration.GenerateSerializerAttribute auf einen Typ angewendet wird, generiert Orleans ebenfalls Kopierer für diesen Typ. Orleans verhindert das Kopieren von Typen oder einzelnen Membern, die mit dem ImmutableAttribute gekennzeichnet sind. Weitere Informationen finden Sie unter Serialisierung unveränderlicher Typen in Orleans.

Bewährte Methoden für die Serialisierung

  • Versehen Sie Ihre Typen mithilfe des Attributs [Alias("my-type")] mit Aliasen. Typen mit Aliasen können ohne Störung der Kompatibilität umbenannt werden.

  • ❌Ändern Sie recordnicht in ein reguläres class-Element oder umgekehrt. Datensätze und Klassen werden nicht auf gleiche Weise abgebildet, da Datensätze neben regulären Membern auch primäre Konstruktormember haben und die beiden daher nicht austauschbar sind.

  • ❌Fügen Sie einer vorhandenen Typhierarchie für einen serialisierbaren Typ keine neuen Typen hinzu. Sie dürfen einem vorhandenen Typ keine neue Basisklasse hinzufügen. Sie können einem vorhandenen Typ eine neue Basisklasse sicher hinzufügen.

  • Ersetzen Sie Verwendungen von SerializableAttribute durch GenerateSerializerAttribute und entsprechende Deklarationen von IdAttribute.

  • Lassen Sie alle Member-IDs für jeden Typ bei 0 beginnen. IDs in einer Unterklasse und ihrer Basisklasse können sich sicher überlappen. Beide Eigenschaften im folgenden Beispiel verfügen über IDs, die gleich 0 sind.

    [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; }
    }
    
  • Erweitern Sie numerische Membertypen nach Bedarf. Sie können sbyte in short in int in long erweitern.

    • Sie können numerische Membertypen einschränken, was jedoch zu einer Laufzeitausnahme führt, wenn beobachtete Werte nicht korrekt durch den eingeschränkten Typ dargestellt werden können. So kann z. B. int.MaxValue nicht vom Feld short abgebildet werden, weshalb die Einschränkung des Felds int auf short zu einer Laufzeitausnahme führen kann, wenn ein solcher Wert angetroffen wird.
  • Ändern Sie nicht die Vorzeichenbehaftung eines numerischen Typelements. Sie dürfen z. B. den Typ eines Members nicht von uint in int oder von int inuint ändern.

Serialisierungsmodule für Grainspeicher

Orleans enthält ein vom Anbieter gestütztes Persistenzmodell für Grains, auf das Sie über die State-Eigenschaft zugreifen oder indem Sie mindestens einen IPersistentState<TState>-Wert in Ihr Grain einfügen. Vor Orleans 7.0 hatte jeder Anbieter einen anderen Mechanismus zum Konfigurieren der Serialisierung. In Orleans 7.0 gibt es jetzt mit IGrainStorageSerializer eine allgemeine Schnittstelle für das Serialisierungsmodul für den Grainstatus, die eine konsistente Möglichkeit bietet, die Statusserialisierung für jeden Anbieter anzupassen. Unterstützte Speicheranbieter implementieren ein Muster, bei dem die Eigenschaft IStorageProviderSerializerOptions.GrainStorageSerializer in der Optionsklasse des Anbieters festgelegt wird. Beispiel:

Die Serialisierung des Grainspeichers wird derzeit standardmäßig auf Newtonsoft.Jsonfestgelegt, um den Zustand zu serialisieren. Sie können diese ersetzen, indem Sie diese Eigenschaft zur Konfigurationszeit ändern. Das folgende Beispiel veranschaulicht dies mithilfe von OptionsBuilder<TOptions>:

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

Weitere Informationen finden Sie unter OptionsBuilder-API.

Orleans verfügt über ein fortschrittliches und erweiterbares Serialisierungsframework. Orleans serialisiert Datentypen, die in Grainanforderungen und Antwortnachrichten übergeben werden, sowie grainbeständige Zustandsobjekte. Im Rahmen dieses Frameworks generiert Orleans automatisch Serialisierungscode für diese Datentypen. Orleans generiert nicht nur eine effizientere Serialisierung/Deserialisierung für Typen, die bereits .NET-serialisierbar sind, sondern versucht auch, Serialisierungsmodule für Typen zu generieren, die in Grainschnittstellen verwendet werden, die nicht .NET-serialisierbar sind. Das Framework enthält außerdem eine Reihe effizienter integrierter Serialisierungsmodule für häufig verwendete Typen: Listen, Wörterbücher, Zeichenfolgen, Primitive, Arrays usw.

Zwei wichtige Features des Serialisierungsmoduls von Orleans legen den Unterschied zu vielen anderen Serialisierungsframeworks von Drittanbietern offen: dynamische Typen/beliebige Polymorphie und Objektidentität.

  1. Dynamische Typen und beliebige Polymorphie:Orleanserzwingt keine Beschränkungen für die Typen, die in Grainaufrufen übergeben werden können, und behält die dynamische Natur des tatsächlichen Datentyps bei. Das bedeutet z. B., dass, wenn die Methode in den Grainschnittstellen so deklariert ist, dass sie IDictionary akzeptiert, der Sender aber zur Runtime SortedDictionary<TKey,TValue> übergibt, der Empfänger tatsächlich SortedDictionary erhält (obwohl der „statische Vertrag“ bzw. die Grainschnittstelle dieses Verhalten nicht festgelegt hat).

  2. Beibehalten der Objektidentität: Wenn dasselbe Objekt in den Argumenten eines Grainaufrufs mehrfach übergeben wird oder die Argumente mehr als einmal indirekt darauf verweisen, wird es von Orleans nur einmal serialisiert. Auf der Empfängerseite wird Orleans alle Verweise ordnungsgemäß wiederherstellen, sodass zwei Zeiger auf dasselbe Objekt auch nach der Deserialisierung noch auf dasselbe Objekt zeigen. In Szenarien wie dem folgenden ist es wichtig, die Objektidentität zu wahren. Stellen Sie sich vor, Grain A sendet ein Wörterbuch mit 100 Einträgen an Grain B, und 10 der Schlüssel im Wörterbuch verweisen auf dasselbe Objekt „obj.“ auf der Seite von A. Ohne Wahrung der Objektidentität würde B ein Wörterbuch mit 100 Einträgen erhalten, wobei die 10 Schlüssel auf 10 verschiedene Klone von „obj“ verweisen. Wenn die Objektidentität gewahrt wird, sieht das Wörterbuch auf der Seite von B genauso aus wie auf der Seite von A, wobei die 10 Schlüssel auf ein einzelnes „obj“-Objekt verweisen.

Die beiden oben genannten Verhaltensweisen werden vom standardmäßigen .NET-Binärserialisierungsmodul zur Verfügung gestellt. Daher war es für uns wichtig, dieses standardmäßige und bekannte Verhalten auch in Orleans zu unterstützen.

Generierte Serialisierungsmodule

Orleans verwendet die folgenden Regeln, um zu entscheiden, welche Serialisierungsmodule generiert werden sollen. Die Regeln lauten:

  1. Überprüfen Sie alle Typen in allen Assemblys, die auf die Kernbibliothek von Orleans verweisen.
  2. Aus diesen Assemblys: Generieren Sie Serialisierungsmodule für Typen, auf die in den Methodensignaturen von Grainschnittstellen oder in der Signatur von Zustandsklassen direkt verwiesen wird, oder für jeden Typ, der mit SerializableAttribute markiert ist.
  3. Darüber hinaus kann eine Grainschnittstelle oder ein Implementierungsprojekt auf beliebige Typen für die Serialisierungsgenerierung verweisen, indem ein KnownTypeAttribute-oder KnownAssemblyAttribute-Attribut auf Assemblyebene hinzugefügt wird, um den Codegenerator anzuweisen, Serialisierungsmodule für bestimmte Typen oder alle in Frage kommenden Typen innerhalb einer Assembly zu generieren. Weitere Informationen zu Attributen auf Assemblyebene finden Sie unter Anwenden von Attributen auf Assemblyebene.

Fallbackserialisierung

Orleans unterstützt die Übertragung beliebiger Typen zur Laufzeit, daher kann der integrierte Codegenerator nicht vorab alle Typen bestimmen, die übertragen werden. Außerdem können für bestimmte Typen keine Serialisierungsmodule generiert werden, da sie unzugänglich sind (z. B. private) oder unzugängliche Felder aufweisen (z. B. readonly). Daher ist eine Just-In-Time-Serialisierung von Typen erforderlich, die unerwartet waren oder für die keine Serialisierungsmodule im Voraus generiert werden konnten. Das für diese Typen zuständige Serialisierungsmodul wird als Fallbackserialisierungsmodul bezeichnet. Orleans bietet zwei Fallbackserialisierungsmodule:

  • Orleans.Serialization.BinaryFormatterSerializer, das BinaryFormatter von .NET verwendet und
  • Orleans.Serialization.ILBasedSerializer, das zur Laufzeit CIL-Anweisungen ausgibt, um Serialisierungsmodule zu erstellen, die das Serialisierungsframework von Orleans zur Serialisierung der einzelnen Felder nutzen. Dies bedeutet, wenn ein unzugänglicher MyPrivateType-Typ ein MyType-Feld enthält, das ein benutzerdefiniertes Serialisierungsmodul aufweist, wird dieses benutzerdefinierte Serialisierungsmodul verwendet, um ihn zu serialisieren.

Das Fallbackserialisierungsmodul kann mithilfe der FallbackSerializationProvider-Eigenschaft sowohl für ClientConfiguration auf dem Client als auch für GlobalConfiguration in den Silos konfiguriert werden.

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

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

Alternativ kann der Anbieter der Fallbackserialisierung in der XML-Konfiguration angegeben werden:

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

BinaryFormatterSerializer ist das standardmäßige Fallbackserialisierungsmodul.

Warnung

Die binäre Serialisierung mit BinaryFormatter kann gefährlich sein. Weitere Informationen finden Sie im BinaryFormatter-Sicherheitshandbuch und im BinaryFormatter-Migrationshandbuch.

Ausnahmeserialisierung

Ausnahmen werden mithilfe des Fallbackserialisierungsmoduls serialisiert. Bei Verwendung der Standardkonfiguration ist BinaryFormatter das Fallbackserialisierungsmodul und daher muss das ISerializable-Muster befolgt werden, um die ordnungsgemäße Serialisierung aller Eigenschaften in einem Ausnahmetyp sicherzustellen.

Nachfolgend sehen Sie ein Beispiel für einen Ausnahmetyp mit ordnungsgemäß implementierter Serialisierung:

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

Bewährte Methoden für die Serialisierung

Die Serialisierung dient in Orleans vor allem zwei Zwecken:

  1. Als Übertragungsformat zum Übertragen von Daten zwischen Grain und Clients zur Laufzeit.
  2. Als Speicherformat für die Aufbewahrung langlebiger Daten zum späteren Abruf.

Die von Orleans generierten Serialisierungsmodule sind aufgrund ihrer Flexibilität, Leistung und Vielseitigkeit für den ersten Zweck geeignet. Für den zweiten Zweck sind sie nicht so gut geeignet, da sie nicht explizit versionstolerant sind. Es wird empfohlen, dass Benutzer ein versionstolerantes Serialisierungsmodul wie Protokollpuffer für beständige Daten konfigurieren. Protokollpuffer werden über Orleans.Serialization.ProtobufSerializer aus dem NuGet-Paket Microsoft.Orleans.OrleansGoogleUtils unterstützt Verwenden Sie die bewährten Methoden für das jeweilige Serialisierungsmodul, um die Versionstoleranz zu gewährleisten. Serialisierungsmodule von Drittanbietern können mithilfe der Konfigurationseigenschaft SerializationProviders konfiguriert werden, wie oben beschrieben.