Aanpassing van serialisatie in Orleans
Een belangrijk aspect hiervan Orleans is de ondersteuning voor aanpassing van serialisatie. Dit is het proces van het converteren van een object of gegevensstructuur naar een indeling die later kan worden opgeslagen of verzonden, en later kan worden gereconstrueerd. Hierdoor kunnen ontwikkelaars bepalen hoe gegevens worden gecodeerd en gedecodeerd wanneer ze worden verzonden tussen verschillende onderdelen van het systeem. Serialisatieaanpassing kan handig zijn voor het optimaliseren van prestaties, interoperabiliteit en beveiliging.
Serialisatieproviders
Orleans biedt twee serialisatie-implementaties:
Als u een van deze pakketten wilt configureren, raadpleegt u de serialisatieconfiguratie in Orleans.
Implementatie van aangepaste serialisatiefunctie
Voor het maken van een aangepaste serialisatie-implementatie zijn enkele veelvoorkomende stappen nodig. U moet verschillende interfaces implementeren en vervolgens uw serializer registreren bij de Orleans runtime. In de volgende secties worden de stappen gedetailleerder beschreven.
Begin met het implementeren van de volgende Orleans serialisatie-interfaces:
- IGeneralizedCodec: Een codec die meerdere typen ondersteunt.
- IGeneralizedCopier: biedt functionaliteit voor het kopiëren van objecten van meerdere typen.
- ITypeFilter: Functionaliteit voor het toestaan van typen die kunnen worden geladen en om deel te nemen aan serialisatie en deserialisatie.
Bekijk het volgende voorbeeld van een aangepaste serializer-implementatie:
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();
}
In de voorgaande voorbeeld-implementatie:
- Elke interface wordt expliciet geïmplementeerd om conflicten met methodenaamomzetting te voorkomen.
- Elke methode genereert een NotImplementedException om aan te geven dat de methode niet is geïmplementeerd. U moet elke methode implementeren om de gewenste functionaliteit te bieden.
De volgende stap bestaat uit het registreren van uw serializer bij de Orleans runtime. Dit wordt meestal bereikt door een aangepaste AddCustomSerializer
extensiemethode uit te breiden ISerializerBuilder en beschikbaar te maken. In het volgende voorbeeld ziet u het typische patroon:
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;
}
}
Aanvullende overwegingen zijn het blootstellen van een overbelasting die aangepaste serialisatieopties accepteert die specifiek zijn voor uw aangepaste implementatie. Deze opties kunnen samen met de registratie in de opbouwfunctie worden geconfigureerd. Deze opties kunnen afhankelijk zijn van de implementatie van uw aangepaste serialisatiefunctie.
Orleans ondersteunt integratie met serializers van derden met behulp van een providermodel. Hiervoor is een implementatie vereist van het IExternalSerializer type dat wordt beschreven in de sectie aangepaste serialisatie van dit artikel. Integraties voor enkele algemene serializers worden naast Orleanselkaar onderhouden, bijvoorbeeld:
- Protocolbuffers: Orleans.Serialization.ProtobufSerializer van Microsoft..Orleans OrleansGoogleUtils NuGet-pakket.
- Bond: Orleans.Serialization.BondSerializer van Microsoft .Orleans. Serialization.Bond NuGet-pakket.
- Newtonsoft.Json: Orleans.Serialization.OrleansJsonSerializer uit de kernbibliotheek Orleans .
Aangepaste implementatie van IExternalSerializer
wordt beschreven in de volgende sectie.
Aangepaste externe serializers
Naast het automatisch genereren van serialisatie kan app-code aangepaste serialisatie bieden voor de typen die worden gekozen. Orleans raadt u aan de automatische serialisatiegeneratie te gebruiken voor het merendeel van uw app-typen en alleen aangepaste serializers te schrijven in zeldzame gevallen wanneer u denkt dat het mogelijk is om betere prestaties te krijgen door handcoderingsserialisaties. In deze opmerking wordt beschreven hoe u dit doet en worden enkele specifieke gevallen geïdentificeerd wanneer dit nuttig kan zijn.
Er zijn drie manieren waarop apps serialisatie kunnen aanpassen:
- Voeg serialisatiemethoden toe aan uw type en markeer deze met de juiste kenmerken (CopierMethodAttribute, SerializerMethodAttribute, DeserializerMethodAttribute). Deze methode heeft de voorkeur voor typen die eigendom zijn van uw app, dat wil zeggen de typen waaraan u nieuwe methoden kunt toevoegen.
- Implementeer
IExternalSerializer
en registreer deze tijdens de configuratietijd. Deze methode is handig voor het integreren van een externe serialisatiebibliotheek. - Schrijf een afzonderlijke statische klasse met aantekeningen met een
[Serializer(typeof(YourType))]
met de 3 serialisatiemethoden erin en dezelfde kenmerken als hierboven. Deze methode is handig voor typen die de app niet bezit, bijvoorbeeld typen die zijn gedefinieerd in andere bibliotheken waarover uw app geen controle heeft.
Elk van deze serialisatiemethoden wordt beschreven in de volgende secties.
Inleiding tot aangepaste serialisatie
Orleans serialisatie vindt plaats in drie fasen:
- Objecten worden onmiddellijk diep gekopieerd om isolatie te garanderen.
- Voordat ze op de draad worden geplaatst, worden objecten geserialiseerd naar een bytestroom van een bericht.
- Wanneer deze aan de doelactivering wordt geleverd, worden objecten opnieuw gemaakt (gedeserialiseerd) van de ontvangen bytestroom.
Gegevenstypen die kunnen worden verzonden in berichten, dat wil gezegd, typen die kunnen worden doorgegeven als methodeargumenten of retourwaarden, moeten gekoppelde routines hebben die deze drie stappen uitvoeren. We verwijzen naar deze routines gezamenlijk als de serializers voor een gegevenstype.
De kopieerfunctie voor een type staat alleen, terwijl de serializer en deserializer een combinatie zijn die samenwerken. U kunt alleen een aangepaste kopieerfunctie of alleen een aangepaste serialisatiefunctie en een aangepaste deserializer opgeven, of u kunt aangepaste implementaties van alle drie bieden.
Serializers worden geregistreerd voor elk ondersteund gegevenstype bij het opstarten van silo's en wanneer een assembly wordt geladen. Registratie is nodig voor aangepaste serialisatieroutines voor een type dat moet worden gebruikt. Serializerselectie is gebaseerd op het dynamische type van het object dat moet worden gekopieerd of geserialiseerd. Daarom is het niet nodig om serializers te maken voor abstracte klassen of interfaces, omdat ze nooit worden gebruikt.
Wanneer een aangepaste serializer schrijven
Een handgeschreven serialisatieroutine zal zelden beter presteren dan de gegenereerde versies. Als u geneigd bent om er een te schrijven, moet u eerst de volgende opties overwegen:
Als er velden of eigenschappen in uw gegevenstypen zijn die niet hoeven te worden geserialiseerd of gekopieerd, kunt u deze markeren met de NonSerializedAttribute. Hierdoor wordt de gegenereerde code deze velden overgeslagen bij het kopiëren en serialiseren. Gebruik ImmutableAttribute en Immutable<T> waar mogelijk om te voorkomen dat onveranderbare gegevens worden gekopieerd. Zie Kopiëren optimaliseren voor meer informatie. Als u het gebruik van de standaard algemene verzamelingstypen vermijdt, moet u dit niet doen. De Orleans runtime bevat aangepaste serializers voor de algemene verzamelingen die gebruikmaken van de semantiek van de verzamelingen om kopiëren, serialiseren en deserialisatie te optimaliseren. Deze verzamelingen hebben ook speciale 'verkorte' representaties in de geserialiseerde bytestroom, wat resulteert in nog meer prestatievoordelen. Een zal bijvoorbeeld
Dictionary<string, string>
sneller zijn dan eenList<Tuple<string, string>>
.Het meest voorkomende geval waarbij een aangepaste serializer een merkbare prestatiewinst kan opleveren, is wanneer er aanzienlijke semantische informatie is gecodeerd in het gegevenstype dat niet beschikbaar is door alleen veldwaarden te kopiëren. Matrices die sparser zijn ingevuld, kunnen bijvoorbeeld vaak efficiënter worden geserialiseerd door de matrix te behandelen als een verzameling index-/waardeparen, zelfs als de app de gegevens bewaart als een volledig gerealiseerde matrix voor de snelheid van de bewerking.
Een belangrijk punt om te doen voordat u een aangepaste serializer schrijft, is ervoor te zorgen dat de gegenereerde serializer uw prestaties pijn doet. Profilering helpt hier een beetje, maar nog waardevoller is het uitvoeren van end-to-end stresstests van uw app met verschillende serialisatiebelastingen om de impact op systeemniveau te meten, in plaats van de micro-impact van serialisatie. Als u bijvoorbeeld een testversie bouwt die geen parameters aan of resultaten van graanmethoden doorgeeft, zoomt u in op de gevolgen van serialisatie en het kopiëren van systeemprestaties.
Serialisatiemethoden toevoegen aan een type
Alle serialisatieroutines moeten worden geïmplementeerd als statische leden van de klasse of struct waaraan ze werken. De hier weergegeven namen zijn niet vereist; registratie is gebaseerd op de aanwezigheid van de respectieve kenmerken, niet op methodenamen. Serialisatiemethoden hoeven niet openbaar te zijn.
Tenzij u alle drie de serialisatieroutines implementeert, moet u uw type markeren met de SerializableAttribute manier waarop de ontbrekende methoden voor u worden gegenereerd.
Kopieerapparaat
Kopieermethoden worden gemarkeerd met het Orleans.CodeGeneration.CopierMethodAttributevolgende:
[CopierMethod]
static private object Copy(object input, ICopyContext context)
{
// ...
}
Kopieerprogramma's zijn meestal de eenvoudigste serialisatieroutines die moeten worden geschreven. Ze nemen een object, gegarandeerd van hetzelfde type als het type waarin de kopieerfunctie is gedefinieerd en moeten een semantisch equivalente kopie van het object retourneren.
Als, als onderdeel van het kopiëren van het object, een subobject moet worden gekopieerd, kunt u dit het beste doen door de SerializationManager.DeepCopyInner routine te gebruiken:
var fooCopy = SerializationManager.DeepCopyInner(foo, context);
Belangrijk
Het is belangrijk om de context van de objectidentiteit te behouden SerializationManager.DeepCopyInnervoor de volledige kopieerbewerking in plaats van SerializationManager.DeepCopyde objectidentiteit.
Objectidentiteit onderhouden
Een belangrijke verantwoordelijkheid van een kopieerroutine is het onderhouden van de objectidentiteit. De Orleans runtime biedt een helperklasse voor dit doel. Voordat u een subobject 'met de hand' kopieert (niet door aan te roepen DeepCopyInner
), controleert u of er als volgt naar is verwezen:
var fooCopy = context.CheckObjectWhileCopying(foo);
if (fooCopy is null)
{
// Actually make a copy of foo
context.RecordObject(foo, fooCopy);
}
De laatste regel is de aanroep naar RecordObject, die vereist is, zodat mogelijke toekomstige verwijzingen naar hetzelfde object als foo
verwijzingen correct worden gevonden door CheckObjectWhileCopying.
Notitie
Dit moet alleen worden gedaan voor klasse-exemplaren, niet struct
exemplaren of .NET-primitieven zoals string
, Uri
en enum
.
Als u subobjecten DeepCopyInner
kopieert, wordt de objectidentiteit voor u verwerkt.
Serializer
Serialisatiemethoden worden gemarkeerd met het Orleans.CodeGeneration.SerializerMethodAttributevolgende:
[SerializerMethod]
static private void Serialize(
object input,
ISerializationContext context,
Type expected)
{
// ...
}
Net als bij kopieerders is het 'invoerobject' dat wordt doorgegeven aan een serialisatiefunctie gegarandeerd een exemplaar van het definiërende type. Het type 'verwacht' kan worden genegeerd; het is gebaseerd op informatie over het gegevensitem voor compilatietijd en wordt op een hoger niveau gebruikt om het typevoorvoegsel in de bytestroom te vormen.
Gebruik de SerializationManager.SerializeInner routine om subobjecten te serialiseren:
SerializationManager.SerializeInner(foo, context, typeof(FooType));
Als er geen specifiek verwacht type voor foo is, kunt u null doorgeven voor het verwachte type.
De BinaryTokenStreamWriter klasse biedt een groot aantal methoden voor het schrijven van gegevens naar de bytestroom. Een exemplaar van de klasse kan worden verkregen via de context.StreamWriter
eigenschap. Zie de klasse voor documentatie.
Deserializer
Deserialisatiemethoden worden gemarkeerd met het Orleans.CodeGeneration.DeserializerMethodAttributevolgende:
[DeserializerMethod]
static private object Deserialize(
Type expected,
IDeserializationContext context)
{
//...
}
Het type 'verwacht' kan worden genegeerd; het is gebaseerd op informatie over het gegevensitem en wordt op een hoger niveau gebruikt om het typevoorvoegsel in de bytestroom te vormen. Het werkelijke type van het object dat moet worden gemaakt, is altijd het type klasse waarin de deserializer is gedefinieerd.
Gebruik de SerializationManager.DeserializeInner routine om subobjecten te deserialiseren:
var foo = SerializationManager.DeserializeInner(typeof(FooType), context);
U kunt ook het volgende doen:
var foo = SerializationManager.DeserializeInner<FooType>(context);
Als er geen specifiek verwacht type voor foo is, gebruikt u de niet-generieke DeserializeInner
variant en geeft u door null
voor het verwachte type.
De BinaryTokenStreamReader klasse biedt een groot aantal methoden voor het lezen van gegevens uit de bytestroom. Een exemplaar van de klasse kan worden verkregen via de context.StreamReader
eigenschap. Zie de klasse voor documentatie.
Een serialisatieprovider schrijven
In deze methode implementeert Orleans.Serialization.IExternalSerializer en voegt u deze toe aan de SerializationProviderOptions.SerializationProviders eigenschap op zowel ClientConfiguration de client GlobalConfiguration als op de silo's. Zie Serialisatieproviders voor meer informatie over de configuratie.
Implementaties van IExternalSerializer
het volgt het patroon dat eerder is beschreven voor serialisatie met de toevoeging van een Initialize
methode en een IsSupportedType
methode die Orleans wordt gebruikt om te bepalen of de serializer een bepaald type ondersteunt. Dit is de interfacedefinitie:
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);
}
Een serializer schrijven voor een afzonderlijk type
In deze methode schrijft u een nieuwe klasse met aantekeningen met een kenmerk [SerializerAttribute(typeof(TargetType))]
, waarbij TargetType
het type is dat wordt geserialiseerd en de 3 serialisatieroutines implementeert. De regels voor het schrijven van deze routines zijn identiek aan die bij het implementeren van de IExternalSerializer
. Orleans gebruikt om [SerializerAttribute(typeof(TargetType))]
te bepalen of deze klasse een serializer is en TargetType
dit kenmerk kan meerdere keren worden opgegeven op dezelfde klasse als deze meerdere typen kan serialiseren. Hieronder ziet u een voorbeeld voor een dergelijke 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;
}
}
Algemene typen serialiseren
De TargetType
parameter van [Serializer(typeof(TargetType))]
kan bijvoorbeeld een open-generic type MyGenericType<T>
zijn. In dat geval moet de serializer-klasse dezelfde algemene parameters hebben als het doeltype. Orleans maakt tijdens runtime een concrete versie van de serializer voor elk betontype MyGenericType<T>
dat wordt geserialiseerd, bijvoorbeeld één voor elk van MyGenericType<int>
en MyGenericType<string>
.
Hints voor het schrijven van serializers en deserializers
Vaak is de eenvoudigste manier om een serializer/deserializer-paar te schrijven door een bytematrix te maken en de matrixlengte naar de stroom te schrijven, gevolgd door de matrix zelf, en vervolgens deserialiseren door het proces om te draaien. Als de matrix een vaste lengte heeft, kunt u deze weglaten uit de stroom. Dit werkt goed wanneer u een gegevenstype hebt dat u compact kunt weergeven en dat geen subobjecten bevat die mogelijk worden gedupliceerd (zodat u zich geen zorgen hoeft te maken over objectidentiteit).
Een andere benadering, die de Orleans runtime gebruikt voor verzamelingen zoals woordenlijsten, werkt goed voor klassen met een aanzienlijke en complexe interne structuur: gebruik instantiemethoden om toegang te krijgen tot de semantische inhoud van het object, serialiseer die inhoud en deserialiseer door de semantische inhoud in te stellen in plaats van de complexe interne status. In deze benadering worden binnenste objecten geschreven met SerializeInner en gelezen met behulp van DeserializeInner. In dit geval is het gebruikelijk om ook een aangepaste kopieermachine te schrijven.
Als u een aangepaste serialisatiefunctie schrijft en deze lijkt op een reeks aanroepen naar SerializeInner voor elk veld in de klasse, hebt u geen aangepaste serializer voor die klasse nodig.