Přizpůsobení serializace v Orleans
Jedním z důležitých Orleans aspektů je jeho podpora pro přizpůsobení serializace, což je proces převodu objektu nebo datové struktury do formátu, který lze uložit nebo přenést a rekonstruovat později. Vývojáři tak můžou řídit, jak se data kódují a dekódují, když se odesílají mezi různými částmi systému. Přizpůsobení serializace může být užitečné pro optimalizaci výkonu, interoperability a zabezpečení.
Zprostředkovatelé serializace
Orleans poskytuje dvě implementace serializátoru:
Chcete-li nakonfigurovat některý z těchto balíčků, viz konfigurace serializace v Orleanssouboru .
Implementace vlastního serializátoru
Pokud chcete vytvořit vlastní implementaci serializátoru, je potřeba provést několik běžných kroků. Musíte implementovat několik rozhraní a pak zaregistrovat serializátor v Orleans modulu runtime. Následující části popisují kroky podrobněji.
Začněte implementací následujících Orleans rozhraní serializace:
- IGeneralizedCodec: Kodek, který podporuje více typů.
- IGeneralizedCopier: Poskytuje funkce pro kopírování objektů více typů.
- ITypeFilter: Funkce umožňující načtení typů a účast v serializaci a deserializaci.
Představte si následující příklad implementace vlastního serializátoru:
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();
}
V předchozí ukázkové implementaci:
- Každé rozhraní je explicitně implementováno, aby nedocházelo ke konfliktům s překladem názvů metod.
- Každá metoda vyvolá NotImplementedException indikaci, že metoda není implementována. Abyste mohli poskytovat požadované funkce, budete muset implementovat každou metodu.
Dalším krokem je registrace serializátoru v modulu Orleans runtime. Toho se obvykle dosahuje rozšířením ISerializerBuilder a zveřejněním vlastní AddCustomSerializer
metody rozšíření. Následující příklad ukazuje typický vzor:
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;
}
}
Dalšími aspekty by bylo zveřejnění přetížení, které přijímá vlastní možnosti serializace specifické pro vaši vlastní implementaci. Tyto možnosti je možné nakonfigurovat spolu s registrací v tvůrci. Tyto možnosti můžou být závislé do implementace vlastního serializátoru.
Orleans podporuje integraci se serializátory třetích stran pomocí modelu zprostředkovatele. To vyžaduje implementaci IExternalSerializer typu popsaného v části vlastní serializace tohoto článku. Integrace pro některé běžné serializátory jsou udržovány vedle Orleans, například:
- Vyrovnávací paměti protokolu: Orleans.Serialization.ProtobufSerializer od microsoftu .Orleans Balíček NuGet OrleansGoogleUtils .
- Bond: Orleans.Serialization.BondSerializer od Microsoftu .Orleans Serialization.Bond NuGet package.
- Newtonsoft.Json: Orleans.Serialization.OrleansJsonSerializer ze základní Orleans knihovny.
Vlastní implementace IExternalSerializer
je popsaná v následující části.
Vlastní externí serializátory
Kromě automatického generování serializace může kód aplikace poskytovat vlastní serializaci pro typy, které si zvolí. Orleans doporučuje používat automatické generování serializace pro většinu typů vaší aplikace a pouze psaní vlastních serializátorů ve výjimečných případech, když se domníváte, že je možné dosáhnout lepšího výkonu ručním kódováním serializátorů. Tato poznámka popisuje, jak to provést, a identifikuje některé konkrétní případy, kdy může být užitečné.
Existují tři způsoby, jak aplikace mohou serializaci přizpůsobit:
- Přidejte do svého typu metody serializace a označte je příslušnými atributy (CopierMethodAttribute, SerializerMethodAttribute, DeserializerMethodAttribute). Tato metoda je vhodnější pro typy, které vaše aplikace vlastní, to znamená typy, ke kterým můžete přidat nové metody.
- Implementujte a zaregistrujte
IExternalSerializer
ho během konfigurace. Tato metoda je užitečná pro integraci externí knihovny serializace. - Napište samostatnou statickou třídu opatřenou
[Serializer(typeof(YourType))]
poznámkami se 3 metodami serializace a stejnými atributy jako výše. Tato metoda je užitečná pro typy, které aplikace nevlastní, například typy definované v jiných knihovnách, které aplikace nemá žádnou kontrolu.
Každá z těchto metod serializace je podrobně popsána v následujících částech.
Úvod k vlastní serializaci
Orleans Serializace probíhá ve třech fázích:
- Objekty se okamžitě zkopírují do hloubky, aby se zajistila izolace.
- Před vložením na drát se objekty serializují do datového proudu bajtů zprávy.
- Při doručení do cílové aktivace se objekty znovu vytvoří (deserializují) z přijatého bajtového streamu.
Datové typy, které mohou být odeslány ve zprávách – to znamená typy, které mohou být předány jako argumenty metody nebo návratové hodnoty – musí mít přidružené rutiny, které provádějí tyto tři kroky. Tyto rutiny se souhrnně označují jako serializátory datového typu.
Kopírka pro typ stojí sama, zatímco serializátor a deserializátor jsou dvojice, která spolupracuje. Můžete zadat pouze vlastní kopírku, nebo jen vlastní serializátor a vlastní deserializátor, nebo můžete poskytnout vlastní implementace všech tří.
Serializátory jsou registrovány pro každý podporovaný datový typ při spuštění sil a při každém načtení sestavení. Registrace je nezbytná pro vlastní rutiny serializátoru pro typ, který se má použít. Výběr serializátoru je založen na dynamickém typu objektu, který se má zkopírovat nebo serializovat. Z tohoto důvodu není nutné vytvářet serializátory pro abstraktní třídy nebo rozhraní, protože nikdy nebudou použity.
Kdy napsat vlastní serializátor
Ručně sestavená rutina serializátoru bude zřídka fungovat lépe než vygenerované verze. Pokud vás láká psát, měli byste nejprve zvážit následující možnosti:
Pokud jsou v datových typech pole nebo vlastnosti, které nemusí být serializovány nebo zkopírovány, můžete je označit znakem NonSerializedAttribute. To způsobí, že vygenerovaný kód při kopírování a serializaci přeskočí tato pole. Používejte ImmutableAttribute a Immutable<T> pokud je to možné, abyste se vyhnuli kopírování neměnných dat. Další informace najdete v tématu Optimalizace kopírování. Pokud se vyhnete použití standardních obecných typů kolekcí, nedělejte to. Modul Orleans runtime obsahuje vlastní serializátory pro obecné kolekce, které používají sémantiku kolekcí k optimalizaci kopírování, serializace a deserializace. Tyto kolekce mají také speciální "zkrácené" reprezentace v serializovaném bajtovém datovém proudu, což vede k ještě více výhodám výkonu. Například
Dictionary<string, string>
bude rychlejší než .List<Tuple<string, string>>
Nejběžnějším případem, kdy vlastní serializátor může poskytnout znatelný nárůst výkonu, je, když jsou významné sémantické informace kódované v datovém typu, které nejsou k dispozici jednoduše zkopírováním hodnot polí. Například pole, která jsou řídce vyplněná, mohou být často efektivněji serializována tím, že pole považují za kolekci dvojic index/hodnota, i když aplikace udržuje data jako plně realizované pole pro rychlost provozu.
Klíčovou věcí, kterou je třeba udělat před napsáním vlastního serializátoru, je zajistit, aby vygenerovaný serializátor ublížil vašemu výkonu. Profilace vám pomůže trochu zde, ale ještě cennější je spouštění komplexních zátěžových testů vaší aplikace s proměnlivými serializačními zatíženími, které měří dopad na úroveň systému, a ne na mikro-dopad serializace. Například sestavení testovací verze, která nepředá žádné parametry metodám zrnitosti nebo výsledky, jednoduše pomocí naskenovaných hodnot na obou stranách se přiblíží dopadu serializace a kopírování na výkon systému.
Přidání metod serializace do typu
Všechny rutiny serializátoru by měly být implementovány jako statické členy třídy nebo struktury, se kterou pracují. Zde uvedené názvy nejsou povinné; registrace je založena na přítomnosti příslušných atributů, nikoli na názvech metod. Všimněte si, že metody serializátoru nemusí být veřejné.
Pokud implementujete všechny tři rutiny serializace, měli byste typ označit tak SerializableAttribute , aby chybějící metody byly generovány za vás.
Kopírka
Metody kopírky jsou označeny příznakem Orleans.CodeGeneration.CopierMethodAttribute:
[CopierMethod]
static private object Copy(object input, ICopyContext context)
{
// ...
}
Kopírky jsou obvykle nejjednodušší rutiny serializátoru pro zápis. Přebírají objekt, který zaručuje, že má stejný typ jako typ, ve které je definován kopírovací objekt, a musí vrátit sémanticky ekvivalentní kopii objektu.
Pokud je potřeba v rámci kopírování objektu zkopírovat dílčí objekt, je nejlepší použít rutinu SerializationManager.DeepCopyInner :
var fooCopy = SerializationManager.DeepCopyInner(foo, context);
Důležité
Je důležité místo toho použít SerializationManager.DeepCopyInnerSerializationManager.DeepCopyk zachování kontextu identity objektu pro úplnou operaci kopírování.
Údržba identity objektu
Důležitou odpovědností rutiny kopírování je udržovat identitu objektu. Modul Orleans runtime poskytuje pomocnou třídu pro tento účel. Před zkopírováním dílčího objektu "ručně" (ne voláním DeepCopyInner
) zkontrolujte, jestli již byl odkazován takto:
var fooCopy = context.CheckObjectWhileCopying(foo);
if (fooCopy is null)
{
// Actually make a copy of foo
context.RecordObject(foo, fooCopy);
}
Poslední řádek je volání RecordObject, což je vyžadováno, aby možné budoucí odkazy na stejný objekt jako foo
odkazy byly nalezeny správně CheckObjectWhileCopying.
Poznámka:
To by se mělo provádět pouze pro instance tříd, nikoli struct
instance nebo primitiv .NET, jako string
jsou , Uri
a enum
.
Pokud používáte DeepCopyInner
ke kopírování dílčích objektů, zpracuje se identita objektu za vás.
Serializátor
Metody serializace jsou označeny příznakem Orleans.CodeGeneration.SerializerMethodAttribute:
[SerializerMethod]
static private void Serialize(
object input,
ISerializationContext context,
Type expected)
{
// ...
}
Stejně jako u kopírek je zaručeno, že "vstupní" objekt předaný serializátoru je instancí definujícího typu. "Očekávaný" typ může být ignorován; je založen na informacích o typu kompilace datové položky a používá se na vyšší úrovni k vytvoření předpony typu v bajtovém streamu.
K serializaci dílčích objektů použijte rutinu SerializationManager.SerializeInner :
SerializationManager.SerializeInner(foo, context, typeof(FooType));
Pokud pro foo neexistuje žádný konkrétní očekávaný typ, můžete předat hodnotu null pro očekávaný typ.
Třída BinaryTokenStreamWriter poskytuje širokou škálu metod pro zápis dat do bajtového datového proudu. Instanci třídy lze získat prostřednictvím context.StreamWriter
vlastnosti. Dokumentaci najdete v této třídě.
Deserializátor
Metody deserializace jsou označeny příznakem Orleans.CodeGeneration.DeserializerMethodAttribute:
[DeserializerMethod]
static private object Deserialize(
Type expected,
IDeserializationContext context)
{
//...
}
"Očekávaný" typ může být ignorován; je založen na informacích o typu kompilace a používá se na vyšší úrovni k vytvoření předpony typu v bajtovém datovém proudu. Skutečný typ objektu, který se má vytvořit, bude vždy typem třídy, ve které je definován deserializátor.
K deserializaci dílčích objektů použijte rutinu SerializationManager.DeserializeInner :
var foo = SerializationManager.DeserializeInner(typeof(FooType), context);
Nebo alternativně:
var foo = SerializationManager.DeserializeInner<FooType>(context);
Pokud pro foo neexistuje žádný konkrétní očekávaný typ, použijte ne generické DeserializeInner
varianty a předejte null
ho očekávanému typu.
Třída BinaryTokenStreamReader poskytuje širokou škálu metod pro čtení dat z bajtového datového proudu. Instanci třídy lze získat prostřednictvím context.StreamReader
vlastnosti. Dokumentaci najdete v této třídě.
Zápis zprostředkovatele serializátoru
V této metodě implementujete Orleans.Serialization.IExternalSerializer a přidáte ji do SerializationProviderOptions.SerializationProviders vlastnosti v ClientConfiguration klientovi i GlobalConfiguration na sila. Informace o konfiguraci naleznete v tématu Zprostředkovatelé serializace.
IExternalSerializer
Implementace se řídí vzorem dříve popsaným pro serializaci s přidáním Initialize
metody a IsSupportedType
metody, která Orleans používá k určení, zda serializátor podporuje daný typ. Toto je definice rozhraní:
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);
}
Zápis serializátoru pro jednotlivé typy
V této metodě napíšete novou třídu anotovanou atributem [SerializerAttribute(typeof(TargetType))]
, kde TargetType
je typ, který je serializován, a implementujte 3 serializační rutiny. Pravidla pro zápis těchto rutin jsou stejná jako při implementaci IExternalSerializer
. Orleans[SerializerAttribute(typeof(TargetType))]
používá k určení, že tato třída je serializátor pro TargetType
a tento atribut lze zadat vícekrát ve stejné třídě, pokud je schopen serializovat více typů. Níže je příklad pro takovou třídu:
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;
}
}
Serializace obecných typů
Parametrem TargetType
[Serializer(typeof(TargetType))]
může být například typ open-generic, MyGenericType<T>
například . V takovém případě musí třída serializátoru mít stejné obecné parametry jako cílový typ. Orleans vytvoří konkrétní verzi serializátoru za běhu pro každý konkrétní MyGenericType<T>
typ, který je serializován, například jeden pro každý z MyGenericType<int>
a MyGenericType<string>
.
Rady pro psaní serializátorů a deserializérů
Nejjednodušším způsobem, jak napsat serializátor/deserializační pár, je serializovat vytvořením bajtového pole a zápisem délky pole do datového proudu, následovaného polem samotným polem a následným deserializací vrácením procesu. Pokud je pole pevné délky, můžete ho vynechat z datového proudu. To funguje dobře, pokud máte datový typ, který můžete kompaktně reprezentovat a který nemá dílčí objekty, které by mohly být duplikované (takže se nemusíte starat o identitu objektu).
Jiný přístup, což je přístup Orleans , který modul runtime přijímá pro kolekce, jako jsou slovníky, funguje dobře pro třídy s významnou a složitou interní strukturou: použití metod instancí pro přístup k sémantickému obsahu objektu, serializaci tohoto obsahu a deserializaci nastavením sémantického obsahu místo komplexního vnitřního stavu. V tomto přístupu se vnitřní objekty zapisují pomocí SerializeInner a čtou pomocí DeserializeInner. V tomto případě je také běžné psát vlastní kopírku.
Pokud napíšete vlastní serializátor a bude vypadat jako posloupnost volání SerializeInner pro každé pole ve třídě, nepotřebujete vlastní serializátor pro tuto třídu.