Anpassen der Serialisierung in Orleans
Ein wichtiger Aspekt von Orleans ist die Unterstützung für die Anpassung der Serialisierung. Dabei handelt es sich um den Prozess der Konvertierung eines Objekts oder einer Datenstruktur in ein Format, das gespeichert oder übertragen und später rekonstruiert werden kann. Auf diese Weise können Entwickler*innen steuern, wie Daten bei der Übertragung zwischen den verschiedenen Teilen eines Systems codiert und decodiert werden. Die Anpassung der Serialisierung kann dabei helfen, Leistung, Interoperabilität und Sicherheit zu optimieren.
Serialisierungsanbieter
Orleans enthält zwei Implementierungen von Serialisierungsprogrammen:
Informationen zur Konfiguration dieser Pakete finden Sie unter Konfigurieren der Serialisierung in Orleans.
Implementierung eines benutzerdefinierten Serialisierungsprogramms
Wenn Sie ein benutzerdefiniertes Serialisierungsprogramm implementieren möchten, müssen Sie einige gängige Schritte ausführen. Sie müssen mehrere Schnittstellen implementieren und anschließend Ihr Serialisierungsprogramm bei der Orleans-Runtime registrieren. In den folgenden Abschnitten werden diese Schritte ausführlicher beschrieben.
Implementieren Sie zunächst die folgenden Orleans-Serialisierungsschnittstellen:
- IGeneralizedCodec: Ein Codec, der mehrere Typen unterstützt.
- IGeneralizedCopier: Bietet Funktionen zum Kopieren von Objekten mehrerer Typen.
- ITypeFilter: Bietet Funktionen, um das Laden von Typen zuzulassen sowie deren Teilnahme an der Serialisierung und Deserialisierung.
Das folgende Beispiel zeigt die Implementierung eines benutzerdefinierten Serialisierungsprogramms:
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();
}
Für die vorherige Beispielimplementierung gilt Folgendes:
- Jede Schnittstelle wird explizit implementiert, um Konflikte mit der Auflösung von Methodennamen zu vermeiden.
- Jede Methode löst eine NotImplementedException aus, um anzugeben, dass die Methode nicht implementiert ist. Sie müssen jede einzelne Methode implementieren, um die gewünschte Funktionalität bereitzustellen.
Im nächsten Schritt registrieren Sie Ihr Serialisierungsprogramm bei der Orleans-Runtime. Erweitern Sie hierfür ISerializerBuilder und machen Sie eine benutzerdefinierte AddCustomSerializer
-Erweiterungsmethode verfügbar. Das typische Muster wird im folgenden Beispiel veranschaulicht:
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;
}
}
Sie können auch eine Überladung verfügbar machen, die benutzerdefinierte Serialisierungsoptionen für Ihre benutzerdefinierte Implementierung akzeptiert. Diese Optionen können zusammen mit der Registrierung im Generator konfiguriert werden. Sie können diese Optionen als Abhängigkeiten in die Implementierung Ihres benutzerdefinierten Serialisierungsprogramms einfügen.
Orleans unterstützt die Integration in Serialisierungsprogramme von Drittanbietern mithilfe eines Anbietermodells. Dies erfordert eine Implementierung des Typs IExternalSerializer, die im Abschnitt zur benutzerdefinierten Serialisierung in diesem Artikel beschrieben wird. Zusammen mit Orleans wird die Integration einiger gängiger Serialisierungsprogramme unterstützt, wie etwa:
- Protokollpuffer: Orleans.Serialization.ProtobufSerializer aus dem Microsoft.Orleans.OrleansGoogleUtils-NuGet-Paket.
- Bond: Orleans.Serialization.BondSerializer aus dem NuGet-Paket Microsoft.Orleans.Serialization.Bond.
- Newtonsoft.Json: Orleans.Serialization.OrleansJsonSerializer aus der Orleans-Kernbibliothek.
Die benutzerdefinierte Implementierung von IExternalSerializer
wird im folgenden Abschnitt beschrieben.
Benutzerdefinierte externe Serialisierungsprogramme
Zusätzlich zur automatischen Serialisierungsgenerierung können Sie mit App-Code eine benutzerdefinierte Serialisierung für die ausgewählten Typen bereitstellen. In Orleans wird für die meisten App-Typen die automatische Serialisierungsgenerierung empfohlen und nur in seltenen Fällen das Schreiben benutzerdefinierter Serialisierungsprogramme, etwa wenn die Leistung dadurch maßgeblich erhöht werden kann. In dieser Anleitung wird beschrieben, wie Sie ein Serialisierungsprogramm schreiben und in welchen Fällen diese Vorgehensweise hilfreich sein kann.
Es gibt drei Möglichkeiten für Apps, die Serialisierung anzupassen:
- Hinzufügen von Serialisierungsmethoden zu Ihrem Typ, die Sie anschließend mit den entsprechenden Attributen markieren (CopierMethodAttribute, SerializerMethodAttribute, DeserializerMethodAttribute): Diese Methode ist für Typen im Besitz Ihrer App geeignet, das bedeutet, dass Sie diesen Typen neue Methoden hinzufügen können.
- Implementieren von
IExternalSerializer
und anschließendes Registrieren während der Konfiguration: Diese Methode ist für die Integration einer externen Serialisierungsbibliothek geeignet. - Schreiben einer separaten statischen Klasse, die mit
[Serializer(typeof(YourType))]
kommentiert ist und die drei Serialisierungsmethoden sowie die gleichen Attributen wie oben enthält: Diese Methode ist für Typen geeignet, die sich nicht im Besitz der App befinden, wie etwa in anderen Bibliotheken definierte Typen, die Ihre App nicht steuern kann.
Die einzelnen Serialisierungsmethoden werden in den folgenden Abschnitten ausführlich beschrieben.
Einführung in die benutzerdefinierte Serialisierung
In Orleans erfolgt die Serialisierung in drei Phasen:
- Von Objekten wird sofort eine tiefe Kopie erstellt, um Isolation sicherzustellen.
- Vor der Übertragung werden Objekte in einen Nachrichtenbytestream serialisiert.
- Bei der Übermittlung an die Zielaktivierung werden Objekte aus dem empfangenen Bytestream neu erstellt (deserialisiert).
Datentypen, die in Nachrichten gesendet werden können (d. h. Typen, die als Methodenargumente oder Rückgabewerte übergeben werden können), müssen über zugeordnete Routinen verfügen, die diese drei Schritte ausführen. Diese Routinen werden zusammen als Serialisierungsprogramme für einen Datentyp bezeichnet.
Das Kopierprogramm für einen Typ arbeitet eigenständig, während das Serialisierungs- und Deserialisierungsprogramm als Paar zusammenarbeiten. Sie können ein benutzerdefiniertes Kopierprogramm oder ein benutzerdefiniertes Serialisierungs- und Deserialisierungsprogramm einzeln oder benutzerdefinierte Implementierungen aller drei Programme bereitstellen.
Serialisierungsprogramme werden für jeden unterstützten Datentyp beim Starten des Silos und beim Laden einer Assembly registriert. Die Registrierung ist für benutzerdefinierte Serialisierungsroutinen für einen Typ erforderlich, der verwendet werden soll. Die Serialisierungsauswahl basiert auf dem dynamischen Typ des Objekts, das kopiert oder serialisiert werden soll. Aus diesem Grund müssen Sie für abstrakte Klassen oder Schnittstellen keine Serialisierungsprogramme erstellen, da diese nie verwendet werden.
Anwendungsfälle für ein benutzerdefiniertes Serialisierungsprogramm
Eine selbst erstellte Serialisierungsroutine hat selten Vorteile gegenüber den generierten Versionen. Daher sollten Sie vor dem Schreiben zunächst folgende Aspekte berücksichtigen:
Felder oder Eigenschaften in Ihren Datentypen, die nicht serialisiert oder kopiert werden müssen, können Sie mit NonSerializedAttribute markieren. Dadurch überspringt der generierte Code diese Felder während des Kopier- und Serialisierungsvorgangs. Verwenden Sie wenn möglich ImmutableAttribute und Immutable<T>, um das Kopieren unveränderlicher Daten zu vermeiden. Weitere Informationen finden Sie unter Optimieren des Kopiervorgangs. Vermeiden Sie nicht die Nutzung der Standardtypen für die generische Auflistung. Die Orleans-Runtime enthält benutzerdefinierte Serialisierungsprogramme für generische Auflistungen, die die Semantik der Auflistungen zur Optimierung des Kopier-, Serialisierungs- und Deserialisierungsvorgangs nutzen. Diese Auflistungen werden zudem im serialisierten Bytestrom „abgekürzt“ dargestellt, was die Leistung weiter erhöht. So wird z. B.
Dictionary<string, string>
schneller ausgeführt alsList<Tuple<string, string>>
.Der häufigste Fall, bei dem ein benutzerdefiniertes Serialisierungsprogramm einen spürbaren Leistungsvorteil bieten kann, sind Datentypen, die wichtige semantische Informationen enthalten, die durch einfaches Kopieren von Feldwerten nicht verfügbar sind. So können z. B. Arrays, die nur wenig gefüllt sind, oft effizienter serialisiert werden, wenn sie als Auflistung von Index-Wert-Paaren behandelt werden. Dies gilt auch dann, wenn die App die Daten für eine schnellere Ausführung als vollständig realisiertes Array beibehält.
Vor dem Schreiben eines benutzerdefinierten Serialisierungsprogramms sollten Sie unbedingt ermitteln, ob das generierte Serialisierungsprogramm die Leistung beeinträchtigt. Eine Profilerstellung kann hier nützlich sein, doch noch wichtiger ist das Ausführen von End-to-End-Belastungstests für Ihre App mit unterschiedlichen Serialisierungslasten, um die Auswirkungen der Serialisierung auf Systemebene anstelle von Mikroauswirkungen zu ermitteln. So vergrößert etwa eine Testversion, die keine Parameter an Grainmethoden übergibt oder Ergebnisse von diesen erhält, nur durch Verwendung gespeicherter Werte auf beiden Seiten die Auswirkungen des Serialisierungs- und Kopiervorgangs auf die Systemleistung.
Hinzufügen von Serialisierungsmethoden zu einem Typ
Alle Serialisierungsroutinen sollten als statische Member der Klasse oder Struktur implementiert werden, für die sie ausgeführt werden. Die hier verwendeten Namen sind nicht erforderlich. Für die Registrierung werden die entsprechenden Attribute benötigt, nicht die Methodennamen. Beachten Sie, dass die Serialisierungsmethoden nicht öffentlich sein müssen.
Wenn Sie nicht alle drei Serialisierungsroutinen implementieren, sollten Sie Ihren Typ mit SerializableAttribute markieren, damit die fehlenden Methoden für Sie generiert werden.
Kopierprogramme
Kopiermethoden werden mit Orleans.CodeGeneration.CopierMethodAttribute gekennzeichnet:
[CopierMethod]
static private object Copy(object input, ICopyContext context)
{
// ...
}
Kopierprogramme sind in der Regel die am einfachsten zu schreibenden Serialisierungsroutinen. Es muss eine semantisch gleichwertige Kopie eines Objekts, das den gleichen Typ aufweist, in dem das Kopierprogramm definiert ist, zurückgegeben werden.
Verwenden Sie die Routine SerializationManager.DeepCopyInner, wenn beim Kopieren eines Objekts ein Unterobjekt kopiert werden muss:
var fooCopy = SerializationManager.DeepCopyInner(foo, context);
Wichtig
Verwenden Sie unbedingt SerializationManager.DeepCopyInner anstelle von SerializationManager.DeepCopy, um den Identitätskontext des Objekts während des gesamten Kopiervorgangs beizubehalten.
Verwalten der Objektidentität
Eine wichtige Aufgabe von Kopierroutinen ist die Verwaltung der Objektidentität. Zu diesem Zweck enthält die Orleans-Runtime eines Hilfsklasse. Bevor Sie ein Unterobjekt manuell kopieren (nicht durch Aufrufen von DeepCopyInner
), überprüfen Sie, ob darauf bereits verwiesen wurde:
var fooCopy = context.CheckObjectWhileCopying(foo);
if (fooCopy is null)
{
// Actually make a copy of foo
context.RecordObject(foo, fooCopy);
}
Die letzte Zeile stellt den Aufruf von RecordObject dar, der erforderlich ist, damit mögliche zukünftige Verweise auf dasselbe Objekt, auf das foo
verweist, ordnungsgemäß von CheckObjectWhileCopying gefunden werden.
Hinweis
Dies gilt nur für Klasseninstanzen, nicht für struct
-Instanzen oder .NET-Primitive wie string
, Uri
und enum
.
Wenn Sie zum Kopieren von Unterobjekten DeepCopyInner
verwenden, wird die Objektidentität für Sie verarbeitet.
serializer
Serialisierungsmethoden werden mit Orleans.CodeGeneration.SerializerMethodAttribute gekennzeichnet:
[SerializerMethod]
static private void Serialize(
object input,
ISerializationContext context,
Type expected)
{
// ...
}
Wie beim Kopierprogramm stellt ein an das Serialisierungsprogramm übergebene Eingabeobjekt garantiert eine Instanz des definierenden Typs dar. Der erwartete Typ kann ignoriert werden. Dieser basiert auf Typinformationen zum Datenelement zur Kompilierzeit und wird auf höherer Ebene für das Typpräfix im Bytestream verwendet.
Verwenden Sie zum Serialisieren von Unterobjekten die Routine SerializationManager.SerializeInner:
SerializationManager.SerializeInner(foo, context, typeof(FooType));
Ist kein bestimmter erwarteter Typ für „foo“ vorhanden, können Sie NULL übergeben.
Die BinaryTokenStreamWriter-Klasse enthält eine Vielzahl von Methoden zum Schreiben von Daten in den Bytestream. Über die Eigenschaft context.StreamWriter
können Sie eine Instanz der Klasse abrufen. Eine Dokumentation hierzu finden Sie in der Klasse.
Deserialisierungsprogramme
Deserialisierungsmethoden werden mit Orleans.CodeGeneration.DeserializerMethodAttribute gekennzeichnet:
[DeserializerMethod]
static private object Deserialize(
Type expected,
IDeserializationContext context)
{
//...
}
Der erwartete Typ kann ignoriert werden. Dieser basiert auf Typinformationen zum Datenelement zur Kompilierzeit und wird auf höherer Ebene für das Typpräfix im Bytestream verwendet. Der tatsächliche Typ des zu erstellenden Objekts ist immer der Typ der Klasse, in der das Deserialisierungsprogramm definiert wird.
Verwenden Sie zum Deserialisieren von Unterobjekten die Routine SerializationManager.DeserializeInner:
var foo = SerializationManager.DeserializeInner(typeof(FooType), context);
Oder auch:
var foo = SerializationManager.DeserializeInner<FooType>(context);
Ist kein bestimmter erwarteter Typ für „foo“ vorhanden, verwenden Sie den nicht generischen Variant-Typ DeserializeInner
und übergeben Sie null
für den erwarteten Typ.
Die BinaryTokenStreamReader-Klasse enthält eine Vielzahl von Methoden zum Lesen von Daten aus dem Bytestream. Über die Eigenschaft context.StreamReader
können Sie eine Instanz der Klasse abrufen. Eine Dokumentation hierzu finden Sie in der Klasse.
Schreiben eines Serialisierungsanbieters
In dieser Methode implementieren Sie Orleans.Serialization.IExternalSerializer und fügen es der Eigenschaft SerializationProviderOptions.SerializationProviders der Klasse ClientConfiguration auf dem Client und der Klasse GlobalConfiguration in den Silos hinzu. Informationen zur Konfiguration finden Sie unter Serialisierungsanbieter.
Implementierungen von IExternalSerializer
folgen dem zuvor beschriebenen Muster für die Serialisierung und fügen eine Initialize
-Methode und eine IsSupportedType
-Methode hinzu. Mit diesen Methoden bestimmt Orleans, ob das Serialisierungsprogramm einen bestimmten Typ unterstützt. Mit dem folgenden Code definieren Sie eine Schnittstelle:
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);
}
Schreiben eines Serialisierungsprogramms für einen einzelnen Typ
In dieser Methode schreiben Sie eine neue Klasse mit dem Attribut [SerializerAttribute(typeof(TargetType))]
, wobei TargetType
den serialisierten Typ darstellt, und implementieren die drei Serialisierungsroutinen. Beim Schreiben dieser Routinen gelten die gleichen Regeln wie bei der Implementierung von IExternalSerializer
. Orleans bestimmt mithilfe von [SerializerAttribute(typeof(TargetType))]
, ob diese Klasse ein Serialisierungsprogramm für TargetType
darstellt und dieses Attribut mehrmals für dieselbe Klasse angegeben werden kann, wenn es mehrere Typen serialisieren kann. Im folgenden Code sehen Sie ein Beispiel für eine solche Klasse:
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;
}
}
Serialisieren generischer Typen
Der TargetType
-Parameter von [Serializer(typeof(TargetType))]
kann ein offener generischer Typ sein, wie etwa MyGenericType<T>
. In diesem Fall muss die Serialisierungsklasse über die gleichen generischen Parameter wie der Zieltyp verfügen. Orleans erstellt für jeden konkreten MyGenericType<T>
-Typ, der serialisiert wird, eine konkrete Version des Serialisierungsprogramms zur Laufzeit, z. B. je eine Version für MyGenericType<int>
und für MyGenericType<string>
.
Hinweise zum Schreiben von Serialisierungs- und Deserialisierungsprogrammen
Häufig besteht die einfachste Möglichkeit zum Schreiben eines Serialisierungs-/Deserialisierungspaars darin, ein Bytearray zu erstellen und die Arraylänge in den Stream zu schreiben, gefolgt vom Array selbst. Anschließend erfolgt der Deserialisierungsvorgang durch Umkehren des Prozesses. Bei einer festen Arraylänge können Sie das Array im Stream weglassen. Diese Vorgehensweise eignet sich gut für Datentypen, die Sie kompakt darstellen können und die keine Unterobjekte aufweisen, die möglicherweise dupliziert werden (sodass Sie sich keine Gedanken über die Objektidentität machen müssen).
Ein weiterer Ansatz (den die Orleans-Runtime für Auflistungen wie Wörterbücher verwendet) eignet sich gut für Klassen mit einer signifikanten und komplexen internen Struktur: Verwenden Sie Instanzmethoden für den Zugriff auf den semantischen Inhalt des Objekts, serialisieren Sie diesen Inhalt, und führen Sie anschließend die Deserialisierung durch, indem Sie den semantischen Inhalt anstelle des komplexen internen Zustands festlegen. Bei dieser Vorgehensweise werden innere Objekte mit „SerializeInner“ geschrieben und mit „DeserializeInner“ gelesen. In diesem Fall ist es üblich, auch ein benutzerdefiniertes Kopierprogramm zu schreiben.
Wenn Sie ein benutzerdefiniertes Serialisierungsprogramm schreiben, das letzten Endes eine Sequenz von SerializeInner-Aufrufen für jedes Feld in der Klasse darstellt, benötigen Sie kein benutzerdefiniertes Serialisierungsprogramm für diese Klasse.