Сериализация в Orleans

В следующих целях используются Orleansдва типа сериализации:

  • Сериализация вызовов зерна — используется для сериализации объектов, передаваемых в зерна и из нее.
  • Сериализация хранилища — используется для сериализации объектов в системы хранения и из нее.

Большая часть этой статьи посвящена сериализации вызовов с помощью платформы сериализации, включенной в Orleans. В разделе сериализаторов хранилища зерна рассматривается сериализация хранилища зерна.

Использование Orleans сериализации

Orleans включает расширенную и расширяемую платформу сериализации, которую можно называть Orleans. Сериализация. Платформа сериализации, включенная в Orleans состав, предназначена для выполнения следующих целей:

  • Высокая производительность — сериализатор разработан и оптимизирован для производительности. Дополнительные сведения доступны в этой презентации.
  • Высокая точность — сериализатор верно представляет большинство. Система типов NET, включая поддержку универсальных, полиморфизма, иерархий наследования, удостоверения объектов и циклических графов. Указатели не поддерживаются, так как они не переносятся между процессами.
  • Гибкость. Сериализатор можно настроить для поддержки сторонних библиотек, создавая суррогаты или делегируя внешние библиотеки сериализации, такие как System.Text.Json, Newtonsoft.Json и Google.Protobuf.
  • Отказоустойчивость версий — сериализатор позволяет типам приложений развиваться с течением времени, поддерживая следующие возможности:
    • Добавление и удаление элементов
    • Подклассирование
    • Числовое расширение и сужение (например, int от/от long, float до/из double)
    • Переименование типов

Высокочувственное представление типов довольно редко для сериализаторов, поэтому некоторые моменты требуют дальнейшего изучения:

  1. Динамические типы и произвольный полиморфизм: Orleans не применяет ограничения на типы, которые могут передаваться в вызовах зерна и поддерживать динамический характер фактического типа данных. Это означает, что, например, если метод в интерфейсах зерна объявлен для принятия IDictionary , но во время выполнения отправитель проходит SortedDictionary<TKey,TValue>, получатель действительно получит SortedDictionary (хотя интерфейс статического контракта или зерна не указал это поведение).

  2. Сохранение удостоверения объекта: если один и тот же объект передает несколько типов в аргументах вызова зерна или косвенно указывает несколько раз из аргументов, Orleans сериализует его только один раз. На стороне получателя все ссылки будут восстановлены правильно, Orleans чтобы два указателя на один и тот же объект по-прежнему указывали на тот же объект после десериализации. Удостоверение объекта важно сохранить в таких сценариях, как показано ниже. Представьте себе, что зерно A отправляет словарь с 100 записями в зерно B, а 10 ключей в словаре указывают на тот же объект, objна стороне A. Без сохранения удостоверения объекта B получит словарь из 100 записей с этими 10 ключами, указывающими на 10 различных клонов obj. При сохранении удостоверения объекта словарь на стороне B выглядит точно так же, как на стороне A с этими 10 ключами, указывающими на один объект obj. Обратите внимание, что поскольку реализации хэш-кода по умолчанию в .NET случайные для каждого процесса, порядок значений в словарях и хэш-наборах (например, не может быть сохранен).

Для поддержки отказоустойчивости версий сериализатор требует, чтобы разработчики были явными сведениями о том, какие типы и члены сериализуются. Мы пытались сделать это как можно более безболезньным. Для создания кода сериализатора для типа необходимо пометить все сериализируемые типы Orleans.GenerateSerializerAttributeOrleans . После этого можно использовать включенное исправление кода, чтобы добавить необходимые Orleans.IdAttribute для сериализуемых элементов в типы, как показано здесь:

An animated image of the available code fix being suggested and applied on the GenerateSerializerAttribute when the containing type doesn't contain IdAttribute's on its members.

Ниже приведен пример сериализуемого типа Orleans, демонстрирующего применение атрибутов.

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

Orleans поддерживает наследование и сериализует отдельные слои в иерархии отдельно, что позволяет им иметь отдельные идентификаторы элементов.

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

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

В приведенном выше коде обратите внимание на то, что оба PublicationBook элемента имеют члены, [Id(0)] несмотря на Book то, что производные от Publicationнего. Это рекомендуемая практика, Orleans так как идентификаторы элементов область на уровень наследования, а не тип в целом. Элементы можно добавлять и удалять из PublicationBook и независимо, но новый базовый класс нельзя вставить в иерархию после развертывания приложения без особого учета.

Orleansтакже поддерживает сериализацию типов с internalэлементами и readonlyprivateэлементами, например в этом примере:

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

По умолчанию Orleans сериализует тип путем кодирования полного имени. Это можно переопределить, добавив Orleans.AliasAttribute. Это приведет к сериализации типа с помощью имени, устойчивого к переименованию базового класса или перемещению его между сборками. Псевдонимы типов глобально область, и вы не можете иметь два псевдонима с одинаковым значением в приложении. Для универсальных типов значение псевдонима должно содержать число универсальных параметров, предшествующих обратному значению, например MyGenericType<T, U> , псевдоним [Alias("mytype`2")].

Сериализация record типов

Элементы, определенные в основном конструкторе записи, имеют неявные идентификаторы по умолчанию. Другими словами, Orleans поддерживает сериализацию record типов. Это означает, что невозможно изменить порядок параметров для уже развернутого типа, так как это нарушает совместимость с предыдущими версиями приложения (в случае последовательного обновления) и сериализованными экземплярами этого типа в хранилище и потоках. Элементы, определенные в тексте типа записи, не совместно используют удостоверения с основными параметрами конструктора.

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

Если вы не хотите, чтобы основные параметры конструктора автоматически включались в качестве сериализуемых полей, можно использовать [GenerateSerializer(IncludePrimaryConstructorParameters = false)].

Суррогаты для сериализации внешних типов

Иногда может потребоваться передать типы между зернами, над которыми у вас нет полного контроля. В таких случаях это может быть непрактично для преобразования в некоторый настраиваемый тип в коде приложения вручную. Orleans предлагает решение для этих ситуаций в виде суррогатных типов. Суррогаты сериализуются вместо их целевого типа и имеют функциональные возможности для преобразования в целевой тип и из него. Рассмотрим следующий пример внешнего типа и соответствующего суррогата и преобразователя:

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

В предыдущем коде:

  • Тип MyForeignLibraryValueType вне элемента управления, определенный в используемой библиотеке.
  • Суррогатный MyForeignLibraryValueTypeSurrogate тип, который сопоставляется с MyForeignLibraryValueType.
  • Указывает RegisterConverterAttribute , что MyForeignLibraryValueTypeSurrogateConverter преобразователь действует как преобразователь для сопоставления с двумя типами. Класс представляет собой реализацию IConverter<TValue,TSurrogate> интерфейса.

Orleans поддерживает сериализацию типов в иерархиях типов (типы, производные от других типов). В случае, если внешний тип может отображаться в иерархии типов (например, в качестве базового класса для одного из собственных типов), необходимо дополнительно реализовать Orleans.IPopulator<TValue,TSurrogate> интерфейс. Рассмотрим следующий пример:

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

Правила управления версиями

Поддержка отказоустойчивости версий обеспечивается, если разработчик следует набору правил при изменении типов. Если разработчик знаком с такими системами, как буферы протокола Google (Protobuf), эти правила будут знакомы.

Составные типы (class & struct)

  • Наследование поддерживается, но изменение иерархии наследования объекта не поддерживается. Базовый класс класса нельзя добавить, изменить на другой или удалить.
  • За исключением некоторых числовых типов, описанных в разделе "Числовые " ниже, типы полей нельзя изменить.
  • Поля можно добавлять или удалять в любой момент в иерархии наследования.
  • Не удается изменить идентификаторы полей.
  • Идентификаторы полей должны быть уникальными для каждого уровня в иерархии типов, но можно повторно использовать между базовыми классами и подклассами. Например, Base класс может объявлять поле с идентификатором 0 и другим полем можно объявить с таким же идентификатором Sub : Base0.

Числовые данные

  • Невозможно изменить подпись числового поля.
    • Преобразования между int > uint недопустимыми.
  • Ширину числового поля можно изменить.
    • Например: поддерживаются преобразования из int или longulong в ushort них.
    • Преобразования, которые сузят ширину, будут вызываться, если значение среды выполнения поля приведет к переполнению.
      • Преобразование из ulongushort нее поддерживается только в том случае, если значение во время выполнения меньше ushort.MaxValue.
      • Преобразования из doublefloat них поддерживаются только в том случае, если значение среды выполнения находится между float.MinValue и float.MaxValue.
      • Аналогично для decimal, который имеет более узкий диапазон, чем оба double и float.

Копиров

Orleans способствует безопасности по умолчанию. Это включает безопасность из некоторых классов ошибок параллелизма. В частности, немедленно копирует объекты, Orleans передаваемые в вызовах зерна по умолчанию. Это копирование упрощается Orleans. Сериализация и при Orleans.CodeGeneration.GenerateSerializerAttribute применении к типу Orleans также создает копии для этого типа. Orleansне будет копировать типы или отдельные члены, помеченные с помощью .ImmutableAttribute Дополнительные сведения см. в разделе Сериализация неизменяемых типов в Orleans.

Рекомендации по сериализации

  • Дайте псевдонимы типов с помощью атрибута [Alias("my-type")] . Типы с псевдонимами можно переименовать без критической совместимости.

  • Не изменяйте record регулярное class или наоборот. Записи и классы не представлены одинаково, так как записи имеют основные члены конструктора в дополнение к обычным членам, поэтому они не взаимозаменяемы.

  • Не добавляйте новые типы в существующую иерархию типов для сериализуемого типа. Не следует добавлять новый базовый класс в существующий тип. Вы можете безопасно добавить новый подкласс в существующий тип.

  • Замените использование SerializableAttributeGenerateSerializerAttribute соответствующими объявлениями.IdAttribute

  • Запустите все идентификаторы элементов с нуля для каждого типа. Идентификаторы в подклассе и его базовый класс могут безопасно перекрываться. Оба свойства в следующем примере имеют идентификаторы равны 0.

    [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; }
    }
    
  • При необходимости расширяйте числовые типы элементов. Вы можете расширить sbyte до shortintlong.

    • Можно сузить числовые типы элементов, но это приведет к исключению среды выполнения, если наблюдаемые значения не могут быть правильно представлены узким типом. Например, int.MaxValue не может быть представлено short полем, поэтому сужение int поля short может привести к исключению среды выполнения при обнаружении такого значения.
  • Не изменяйте подпись элемента числового типа. Например, не следует изменять тип члена с uintint или на тип intuint.

Сериализаторы хранилища зерна

Orleans включает в себя модель сохраняемости с поддержкой поставщика для зерна, доступ к которым осуществляется через State свойство или путем внедрения одного или нескольких IPersistentState<TState> значений в зерно. До Orleans версии 7.0 у каждого поставщика был другой механизм настройки сериализации. В Orleans версии 7.0 теперь существует интерфейс сериализатора состояния общего назначения, IGrainStorageSerializerкоторый обеспечивает согласованный способ настройки сериализации состояния для каждого поставщика. Поддерживаемые поставщики хранилища реализуют шаблон, который включает настройку IStorageProviderSerializerOptions.GrainStorageSerializer свойства в классе параметров поставщика, например:

Сериализация хранилища в настоящее время используется по умолчанию для Newtonsoft.Json сериализации состояния. Это можно заменить, изменив это свойство во время настройки. В следующем примере показано, как использовать OptionsBuilder<TOptions>:

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

Дополнительные сведения см. в разделе API OptionsBuilder.

Orleans имеет расширенную и расширяемую платформу сериализации. Orleans сериализует типы данных, передаваемые в сообщениях запроса и ответах, а также объекты сохраняемого состояния. В рамках этой платформы Orleans автоматически создает код сериализации для этих типов данных. Помимо создания более эффективной сериализации и десериализации для уже существующих типов. Net-serializable также пытается создать сериализаторы для типов, Orleans используемых в интерфейсах зерна, которые не являются. NET-serializable. Платформа также включает набор эффективных встроенных сериализаторов для часто используемых типов: списки, словари, строки, примитивы, массивы и т. д.

Две важные функции Orleansсериализатора задают его отдельно от множества других сторонних платформ сериализации: динамические типы или произвольный полиморфизм и удостоверение объекта.

  1. Динамические типы и произвольный полиморфизм: Orleans не применяет ограничения на типы, которые могут передаваться в вызовах зерна и поддерживать динамический характер фактического типа данных. Это означает, что, например, если метод в интерфейсах зерна объявлен для принятия IDictionary , но во время выполнения отправитель проходит SortedDictionary<TKey,TValue>, получатель действительно получит SortedDictionary (хотя интерфейс статического контракта или зерна не указал это поведение).

  2. Сохранение удостоверения объекта: если один и тот же объект передает несколько типов в аргументах вызова зерна или косвенно указывает несколько раз из аргументов, Orleans сериализует его только один раз. На стороне получателя все ссылки будут восстановлены правильно, Orleans чтобы два указателя на один и тот же объект по-прежнему указывали на тот же объект после десериализации. Удостоверение объекта важно сохранить в таких сценариях, как показано ниже. Представьте себе, что зерно A отправляет словарь с 100 записями в зерно B, а 10 ключей в словаре указывают на тот же объект, obj, на стороне A. Без сохранения удостоверения объекта B получит словарь из 100 записей с этими 10 ключами, указывающими на 10 различных клонов obj. При сохранении удостоверения объекта словарь на стороне B выглядит точно так же, как на стороне A с этими 10 ключами, указывающими на один объект obj.

Приведенные выше два поведения предоставляются стандартным двоичным сериализатором .NET и поэтому важно для нас поддерживать это стандартное и знакомое поведение Orleans .

Созданные сериализаторы

Orleans использует следующие правила, чтобы решить, какие сериализаторы необходимо создать. Ниже приведены правила.

  1. Сканируйте все типы во всех сборках, ссылающихся на основную Orleans библиотеку.
  2. Из этих сборок создайте сериализаторы для типов, на которые напрямую ссылаются сигнатуры методов сигнатуры или сигнатуры класса состояний, или для любого типа, помеченного как SerializableAttribute.
  3. Кроме того, для создания сериализации можно указать произвольные типы интерфейса или реализации интерфейса или реализации, добавив KnownTypeAttributeKnownAssemblyAttribute атрибуты уровня сборки, чтобы сообщить генератору кода создавать сериализаторы для определенных типов или всех подходящих типов в сборке. Дополнительные сведения об атрибутах уровня сборки см. в разделе "Применение атрибутов на уровне сборки".

Резервная сериализация

Orleans поддерживает передачу произвольных типов во время выполнения, поэтому встроенный генератор кода не может определить весь набор типов, которые будут передаваться заранее. Кроме того, некоторые типы не могут создавать сериализаторы для них, так как они недоступны (например, private) или имеют недоступные поля (например, readonly). Таким образом, существует необходимость jit-сериализации типов, которые были непредвиденными или не могли создавать сериализаторы заранее. Сериализатор, отвечающий за эти типы, называется резервным сериализатором. Orleans поставляется с двумя резервными сериализаторами:

  • Orleans.Serialization.BinaryFormatterSerializer, который использует . BinaryFormatterNET ; и
  • Orleans.Serialization.ILBasedSerializer, который выдает инструкции CIL во время выполнения для создания сериализаторов, использующих Orleansплатформу сериализации для сериализации каждого поля. Это означает, что если недоступный тип MyPrivateType содержит поле MyType с пользовательским сериализатором, то для сериализации будет использоваться пользовательский сериализатор.

Резервный сериализатор можно настроить с помощью FallbackSerializationProvider свойства как на клиенте, так ClientConfiguration и GlobalConfiguration в силосах.

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

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

Кроме того, резервный поставщик сериализации можно указать в конфигурации XML:

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

Это BinaryFormatterSerializer резервный сериализатор по умолчанию.

Сериализация исключений

Исключения сериализуются с помощью резервного сериализатора. При использовании конфигурации по умолчанию используется резервный сериализатор, BinaryFormatter поэтому необходимо выполнить шаблон ISerializable, чтобы обеспечить правильную сериализацию всех свойств в типе исключения.

Ниже приведен пример типа исключения с правильной реализацией сериализации:

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

Рекомендации по сериализации

Сериализация служит двум основным целям в Orleansследующих целях:

  1. В качестве формата провода для передачи данных между зернами и клиентами во время выполнения.
  2. В качестве формата хранилища для сохранения долгоживующих данных для последующего извлечения.

Сериализаторы, созданные компанией Orleans , подходят для первой цели из-за их гибкости, производительности и универсальности. Они не так подходят для второй цели, так как они не являются явным образом терпимыми к версиям. Рекомендуется настроить терпимый к версиям сериализатор, например буферы протокола для постоянных данных. Буферы протокола поддерживаются из Orleans.Serialization.ProtobufSerializerMicrosoft.Orleans.OrleansПакет NuGet GoogleUtils . Для обеспечения допустимости версий следует использовать рекомендации по выбору конкретного сериализатора. Сторонние сериализаторы можно настроить с помощью SerializationProviders свойства конфигурации, как описано выше.