Dostosowywanie serializacji w programie Orleans

Jednym z ważnych aspektów Orleans jest jego obsługa dostosowywania serializacji, która jest procesem konwertowania obiektu lub struktury danych na format, który można przechowywać lub przesyłać i rekonstruować później. Dzięki temu deweloperzy mogą kontrolować sposób kodowania i dekodowania danych podczas ich wysyłania między różnymi częściami systemu. Dostosowywanie serializacji może być przydatne do optymalizacji wydajności, współdziałania i zabezpieczeń.

Dostawcy serializacji

Orleans udostępnia dwie implementacje serializatora:

Aby skonfigurować jeden z tych pakietów, zobacz Konfiguracja serializacji w programie Orleans.

Implementacja niestandardowego serializatora

Aby utworzyć niestandardową implementację serializatora, należy wykonać kilka typowych kroków. Musisz zaimplementować kilka interfejsów, a następnie zarejestrować serializator w środowisku uruchomieniowym Orleans . W poniższych sekcjach opisano bardziej szczegółowo kroki.

Zacznij od zaimplementowania następujących Orleans interfejsów serializacji:

  • IGeneralizedCodec: koder kodujący obsługujący wiele typów.
  • IGeneralizedCopier: udostępnia funkcje kopiowania obiektów wielu typów.
  • ITypeFilter: Funkcjonalność umożliwiająca ładowanie typów i uczestnictwo w serializacji i deserializacji.

Rozważmy następujący przykład niestandardowej implementacji serializatora:

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

W poprzedniej przykładowej implementacji:

  • Każdy interfejs jest jawnie implementowany, aby uniknąć konfliktów z rozpoznawaniem nazw metod.
  • Każda metoda zgłasza wartość , NotImplementedException aby wskazać, że metoda nie jest zaimplementowana. Aby zapewnić odpowiednią funkcjonalność, należy zaimplementować każdą metodę.

Następnym krokiem jest zarejestrowanie serializatora w środowisku uruchomieniowym Orleans . Jest to zwykle osiągane przez rozszerzanie ISerializerBuilder i uwidacznianie niestandardowej AddCustomSerializer metody rozszerzenia. W poniższym przykładzie przedstawiono typowy wzorzec:

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

Dodatkowe zagadnienia to uwidocznienie przeciążenia, które akceptuje niestandardowe opcje serializacji specyficzne dla implementacji niestandardowej. Te opcje można skonfigurować wraz z rejestracją w konstruktorze. Te opcje mogą być wstrzykiwane do niestandardowej implementacji serializatora.

Orleans obsługuje integrację z serializatorami innych firm przy użyciu modelu dostawcy. Wymaga to implementacji typu opisanego IExternalSerializer w sekcji serializacji niestandardowej tego artykułu. Integracje niektórych typowych serializatorów są obsługiwane razem z usługą Orleans, na przykład:

Niestandardowa implementacja IExternalSerializer programu została opisana w poniższej sekcji.

Niestandardowe serializatory zewnętrzne

Oprócz automatycznego generowania serializacji kod aplikacji może zapewnić niestandardową serializacji dla wybieranych typów. Orleans Zaleca używanie automatycznego generowania serializacji dla większości typów aplikacji i tylko pisanie niestandardowych serializatorów w rzadkich przypadkach, gdy uważasz, że jest możliwe zwiększenie wydajności przez serializatory ręcznego kodowania. W tej notatce opisano, jak to zrobić, i identyfikuje określone przypadki, gdy może to być przydatne.

Istnieją trzy sposoby dostosowywania serializacji aplikacji:

  1. Dodaj metody serializacji do typu i oznacz je odpowiednimi atrybutami (CopierMethodAttribute, SerializerMethodAttribute, DeserializerMethodAttribute). Ta metoda jest preferowana w przypadku typów, które należą do aplikacji, czyli typów, do których można dodać nowe metody.
  2. Zaimplementuj IExternalSerializer i zarejestruj go w czasie konfiguracji. Ta metoda jest przydatna do integrowania zewnętrznej biblioteki serializacji.
  3. Napisz oddzielną klasę statyczną z adnotacjami z [Serializer(typeof(YourType))] 3 metodami serializacji w niej i tymi samymi atrybutami co powyżej. Ta metoda jest przydatna w przypadku typów, których aplikacja nie jest właścicielem, na przykład typów zdefiniowanych w innych bibliotekach, nad którymi aplikacja nie ma kontroli.

Każda z tych metod serializacji jest szczegółowo opisanych w poniższych sekcjach.

Wprowadzenie do serializacji niestandardowej

Orleans serializacji odbywa się w trzech etapach:

  • Obiekty są natychmiast głęboko kopiowane w celu zapewnienia izolacji.
  • Przed umieszczeniem na przewodzie obiekty są serializowane do strumienia bajtów komunikatów.
  • Po dostarczeniu do aktywacji docelowej obiekty są ponownie tworzone (deserializowane) ze strumienia bajtów odebranych.

Typy danych, które mogą być wysyłane w komunikatach — czyli typy, które mogą być przekazywane jako argumenty metody lub zwracane wartości — muszą mieć skojarzone procedury, które wykonują te trzy kroki. Te procedury są określane zbiorczo jako serializatory dla typu danych.

Kopiarka dla typu jest sama, podczas gdy serializator i deserializator to para, która współdziała ze sobą. Możesz podać tylko niestandardową kopiarkę lub tylko niestandardowy serializator i niestandardowy deserializator albo udostępnić niestandardowe implementacje wszystkich trzech.

Serializatory są rejestrowane dla każdego obsługiwanego typu danych podczas uruchamiania silosu i za każdym razem, gdy zestaw jest ładowany. Rejestracja jest niezbędna do wykonywania niestandardowych procedur serializacji dla typu, który ma być używany. Wybór serializatora jest oparty na dynamicznym typie obiektu do skopiowania lub serializacji. Z tego powodu nie ma potrzeby tworzenia serializatorów dla klas abstrakcyjnych lub interfejsów, ponieważ nigdy nie będą używane.

Kiedy należy napisać niestandardowy serializator

Ręcznie spreparowana rutyna serializatora rzadko działa lepiej niż wygenerowane wersje. Jeśli jesteś kuszony do napisania, najpierw należy wziąć pod uwagę następujące opcje:

  • Jeśli istnieją pola lub właściwości w typach danych, które nie muszą być serializowane lub kopiowane, możesz oznaczyć je za pomocą elementu NonSerializedAttribute. Spowoduje to pominięcie wygenerowanych pól podczas kopiowania i serializacji kodu. Użyj funkcji ImmutableAttribute i Immutable<T> , jeśli to możliwe, aby uniknąć kopiowania niezmiennych danych. Aby uzyskać więcej informacji, zobacz Optymalizowanie kopiowania. Jeśli unikasz używania standardowych typów kolekcji ogólnych, nie. Środowisko Orleans uruchomieniowe zawiera niestandardowe serializatory dla kolekcji ogólnych, które używają semantyki kolekcji do optymalizacji kopiowania, serializacji i deserializacji. Te kolekcje mają również specjalne "skrócone" reprezentacje w serializowanym strumieniu bajtów, co daje jeszcze większą wydajność. Na przykład wartość a będzie szybsza Dictionary<string, string>List<Tuple<string, string>>niż .

  • Najczęstszym przypadkiem, w którym niestandardowy serializator może zapewnić zauważalny wzrost wydajności, jest to, że w typie danych zakodowane są istotne informacje semantyczne zakodowane w typie danych, który nie jest dostępny, po prostu kopiując wartości pól. Na przykład tablice, które są słabo wypełnione, mogą być często bardziej wydajne serializowane, traktując tablicę jako kolekcję par indeksu/wartości, nawet jeśli aplikacja przechowuje dane jako w pełni zrealizowaną tablicę na potrzeby szybkości działania.

  • Kluczową rzeczą do zrobienia przed napisaniem niestandardowego serializatora jest upewnienie się, że wygenerowany serializator boli wydajność. Profilowanie pomoże nieco tutaj, ale jeszcze bardziej cenne jest uruchamianie kompleksowego testu obciążeniowego aplikacji z różną serializacji obciążenia, aby ocenić wpływ na poziom systemu, a nie mikro-wpływ serializacji. Na przykład utworzenie wersji testowej, która nie przekazuje żadnych parametrów lub wyników z metod ziarna, po prostu przy użyciu wartości w puszce na końcu, zwiększy wpływ serializacji i kopiowania na wydajność systemu.

Dodawanie metod serializacji do typu

Wszystkie procedury serializacji powinny być implementowane jako statyczne elementy członkowskie klasy lub struktury, na których działają. Nazwy pokazane tutaj nie są wymagane; rejestracja jest oparta na obecności odpowiednich atrybutów, a nie na nazwach metod. Należy pamiętać, że metody serializatora nie muszą być publiczne.

Jeśli nie wdrożysz wszystkich trzech procedur serializacji, należy oznaczyć typ za SerializableAttribute pomocą metody tak, aby brakujące metody zostały wygenerowane.

Kopiarki

Metody kopiarki są oflagowane za pomocą polecenia Orleans.CodeGeneration.CopierMethodAttribute:

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

Kopie są zwykle najprostszymi procedurami serializacji do zapisu. Przyjmują obiekt, gwarantowany jako typ, w jakim jest zdefiniowana kopiarka, i muszą zwrócić semantycznie równoważną kopię obiektu.

Jeśli w ramach kopiowania obiektu należy skopiować obiekt podrzędny, najlepszym sposobem na to jest użycie SerializationManager.DeepCopyInner procedury:

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

Ważne

Ważne jest, aby używać elementu SerializationManager.DeepCopyInner, a nie SerializationManager.DeepCopy, aby zachować kontekst tożsamości obiektu dla operacji pełnej kopiowania.

Obsługa tożsamości obiektu

Ważną obowiązkiem procedury kopiowania jest utrzymanie tożsamości obiektu. W Orleans tym celu środowisko uruchomieniowe udostępnia klasę pomocnika. Przed skopiowaniem obiektu podrzędnego "ręcznie" (nie przez wywołanie metody DeepCopyInner), sprawdź, czy został już odwołany w następujący sposób:

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

Ostatnim wierszem jest wywołanie RecordObjectmetody , która jest wymagana, aby możliwe przyszłe odwołania do tego samego obiektu, co foo odwołania, były prawidłowo znajdowane przez CheckObjectWhileCopyingelement .

Uwaga

Należy to zrobić tylko w przypadku wystąpień klas, a niestruct wystąpień ani elementów pierwotnych platformy .NET, takich jak string, Urii enum.

Jeśli używasz DeepCopyInner metody do kopiowania obiektów podrzędnych, tożsamość obiektu jest obsługiwana za Ciebie.

Serializer

Metody serializacji są oflagowane za pomocą elementu Orleans.CodeGeneration.SerializerMethodAttribute:

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

Podobnie jak w przypadku kopierów, obiekt "input" przekazany do serializatora ma gwarancję wystąpienia typu definiującego. Typ "oczekiwany" może być ignorowany; Jest on oparty na informacjach o typie czasu kompilacji dotyczących elementu danych i jest używany na wyższym poziomie do utworzenia prefiksu typu w strumieniu bajtowym.

Aby serializować obiekty podrzędne, użyj SerializationManager.SerializeInner procedury:

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

Jeśli dla foo nie ma określonego oczekiwanego typu, możesz przekazać wartość null dla oczekiwanego typu.

Klasa BinaryTokenStreamWriter udostępnia szeroką gamę metod zapisywania danych do strumienia bajtów. Wystąpienie klasy można uzyskać za pośrednictwem context.StreamWriter właściwości . Zobacz klasę, aby uzyskać dokumentację.

Deserializacji

Metody deserializacji są oflagowane za pomocą polecenia Orleans.CodeGeneration.DeserializerMethodAttribute:

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

Typ "oczekiwany" może być ignorowany; Jest on oparty na informacjach o typie czasu kompilacji o elemencie danych i jest używany na wyższym poziomie do utworzenia prefiksu typu w strumieniu bajtów. Rzeczywisty typ tworzonego obiektu będzie zawsze typem klasy, w której zdefiniowano deserializator.

Aby wykonać deserializowanie obiektów podrzędnych, użyj SerializationManager.DeserializeInner procedury:

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

Alternatywnie:

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

Jeśli dla foo nie ma określonego oczekiwanego typu, użyj wariantu innego niż ogólny DeserializeInner i przekaż null oczekiwany typ.

Klasa BinaryTokenStreamReader udostępnia szeroką gamę metod odczytywania danych ze strumienia bajtów. Wystąpienie klasy można uzyskać za pośrednictwem context.StreamReader właściwości . Zobacz klasę, aby uzyskać dokumentację.

Pisanie dostawcy serializatora

W tej metodzie zaimplementujesz Orleans.Serialization.IExternalSerializer i dodasz ją do SerializationProviderOptions.SerializationProviders właściwości zarówno ClientConfiguration na kliencie, jak i GlobalConfiguration w silosach. Aby uzyskać informacje na temat konfiguracji, zobacz Dostawcy serializacji.

Implementacje są zgodne ze IExternalSerializer wzorcem opisanym wcześniej na potrzeby serializacji z dodatkiem Initialize metody i IsSupportedType metody, która Orleans używa metody w celu określenia, czy serializator obsługuje dany typ. Jest to definicja interfejsu:

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

Pisanie serializatora dla pojedynczego typu

W tej metodzie napiszesz nową klasę z adnotacjami z atrybutem [SerializerAttribute(typeof(TargetType))], gdzie TargetType jest typem, który jest serializowany, i implementujesz 3 procedury serializacji. Reguły dotyczące pisania tych procedur są identyczne z regułami podczas implementowania elementu IExternalSerializer. Orleans[SerializerAttribute(typeof(TargetType))] używa klasy , aby określić, że ta klasa jest serializatorem i TargetType ten atrybut można określić wiele razy w tej samej klasie, jeśli jest w stanie serializować wiele typów. Poniżej przedstawiono przykład takiej klasy:

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

Serializowanie typów ogólnych

Parametr TargetType może [Serializer(typeof(TargetType))] być typem typu open-generic, na przykład MyGenericType<T>. W takim przypadku klasa serializatora musi mieć takie same parametry ogólne jak typ docelowy. Orleans utworzy konkretną wersję serializatora w czasie wykonywania dla każdego typu betonowego MyGenericType<T> , który jest serializowany, na przykład jeden dla każdego typu MyGenericType<int> i MyGenericType<string>.

Wskazówki dotyczące pisania serializatorów i deserializacji

Często najprostszym sposobem na napisanie pary serializatora/deserializatora jest serializacji przez skonstruowanie tablicy bajtów i zapisanie długości tablicy do strumienia, a następnie samej tablicy, a następnie deserializowanie przez odwrócenie procesu. Jeśli tablica ma stałą długość, możesz pominąć ją ze strumienia. Działa to dobrze, gdy masz typ danych, który można reprezentować kompaktowo i nie ma obiektów podrzędnych, które mogą być zduplikowane (więc nie musisz martwić się o tożsamość obiektu).

Innym podejściem, które jest podejście Orleans , które środowisko uruchomieniowe przyjmuje dla kolekcji, takich jak słowniki, dobrze sprawdza się w przypadku klas o znaczącej i złożonej strukturze wewnętrznej: użyj metod wystąpienia, aby uzyskać dostęp do semantycznej zawartości obiektu, serializować ją i deserializować, ustawiając zawartość semantyczną, a nie złożony stan wewnętrzny. W tym podejściu obiekty wewnętrzne są zapisywane przy użyciu klasy SerializeInner i odczytywane przy użyciu deserializeInner. W takim przypadku często pisze się również niestandardową kopiarkę.

Jeśli piszesz niestandardowy serializator i wygląda jak sekwencja wywołań serializeInner dla każdego pola w klasie, nie potrzebujesz niestandardowego serializatora dla tej klasy.

Zobacz też