Сериализация в Orleans
В следующих целях используются Orleansдва типа сериализации:
- Сериализация вызовов зерна — используется для сериализации объектов, передаваемых в зерна и из нее.
- Сериализация хранилища — используется для сериализации объектов в системы хранения и из нее.
Большая часть этой статьи посвящена сериализации вызовов с помощью платформы сериализации, включенной в Orleans. В разделе сериализаторов хранилища зерна рассматривается сериализация хранилища зерна.
Использование Orleans сериализации
Orleans включает расширенную и расширяемую платформу сериализации, которую можно называть Orleans. Сериализация. Платформа сериализации, включенная в Orleans состав, предназначена для выполнения следующих целей:
- Высокая производительность — сериализатор разработан и оптимизирован для производительности. Дополнительные сведения доступны в этой презентации.
- Высокая точность — сериализатор верно представляет большинство. Система типов NET, включая поддержку универсальных, полиморфизма, иерархий наследования, удостоверения объектов и циклических графов. Указатели не поддерживаются, так как они не переносятся между процессами.
- Гибкость. Сериализатор можно настроить для поддержки сторонних библиотек, создавая суррогаты или делегируя внешние библиотеки сериализации, такие как System.Text.Json, Newtonsoft.Json и Google.Protobuf.
- Отказоустойчивость версий — сериализатор позволяет типам приложений развиваться с течением времени, поддерживая следующие возможности:
- Добавление и удаление элементов
- Подклассирование
- Числовое расширение и сужение (например,
int
от/отlong
,float
до/изdouble
) - Переименование типов
Высокочувственное представление типов довольно редко для сериализаторов, поэтому некоторые моменты требуют дальнейшего изучения:
Динамические типы и произвольный полиморфизм: Orleans не применяет ограничения на типы, которые могут передаваться в вызовах зерна и поддерживать динамический характер фактического типа данных. Это означает, что, например, если метод в интерфейсах зерна объявлен для принятия IDictionary , но во время выполнения отправитель проходит SortedDictionary<TKey,TValue>, получатель действительно получит
SortedDictionary
(хотя интерфейс статического контракта или зерна не указал это поведение).Сохранение удостоверения объекта: если один и тот же объект передает несколько типов в аргументах вызова зерна или косвенно указывает несколько раз из аргументов, Orleans сериализует его только один раз. На стороне получателя все ссылки будут восстановлены правильно, Orleans чтобы два указателя на один и тот же объект по-прежнему указывали на тот же объект после десериализации. Удостоверение объекта важно сохранить в таких сценариях, как показано ниже. Представьте себе, что зерно A отправляет словарь с 100 записями в зерно B, а 10 ключей в словаре указывают на тот же объект,
obj
на стороне A. Без сохранения удостоверения объекта B получит словарь из 100 записей с этими 10 ключами, указывающими на 10 различных клоновobj
. При сохранении удостоверения объекта словарь на стороне B выглядит точно так же, как на стороне A с этими 10 ключами, указывающими на один объектobj
. Обратите внимание, что поскольку реализации хэш-кода по умолчанию в .NET случайные для каждого процесса, порядок значений в словарях и хэш-наборах (например, не может быть сохранен).
Для поддержки отказоустойчивости версий сериализатор требует, чтобы разработчики были явными сведениями о том, какие типы и члены сериализуются. Мы пытались сделать это как можно более безболезньным. Для создания кода сериализатора для типа необходимо пометить все сериализируемые типы Orleans.GenerateSerializerAttribute Orleans . После этого можно использовать включенное исправление кода, чтобы добавить необходимые Orleans.IdAttribute для сериализуемых элементов в типы, как показано здесь:
Ниже приведен пример сериализуемого типа 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; }
}
В приведенном выше коде обратите внимание на то, что оба Publication
Book
элемента имеют члены, [Id(0)]
несмотря на Book
то, что производные от Publication
него. Это рекомендуемая практика, Orleans так как идентификаторы элементов относятся к уровню наследования, а не к типу в целом. Элементы можно добавлять и удалять из Publication
Book
и независимо, но новый базовый класс нельзя вставить в иерархию после развертывания приложения без особого учета.
Orleansтакже поддерживает сериализацию типов с internal
элементами и readonly
private
элементами, например в этом примере:
[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 : Base
0
.
Числовые данные
- Невозможно изменить подпись числового поля.
- Преобразования между
int
>uint
недопустимыми.
- Преобразования между
- Ширину числового поля можно изменить.
- Например: поддерживаются преобразования из
int
илиlong
ulong
вushort
них. - Преобразования, которые сузят ширину, будут вызываться, если значение среды выполнения поля приведет к переполнению.
- Преобразование из
ulong
ushort
нее поддерживается только в том случае, если значение во время выполнения меньшеushort.MaxValue
. - Преобразования из
double
float
них поддерживаются только в том случае, если значение среды выполнения находится междуfloat.MinValue
иfloat.MaxValue
. - Аналогично для
decimal
, который имеет более узкий диапазон, чем обаdouble
иfloat
.
- Преобразование из
- Например: поддерживаются преобразования из
Копиров
Orleans способствует безопасности по умолчанию. Это включает безопасность из некоторых классов ошибок параллелизма. В частности, немедленно копирует объекты, Orleans передаваемые в вызовах зерна по умолчанию. Это копирование упрощается Orleans. Сериализация и при Orleans.CodeGeneration.GenerateSerializerAttribute применении к типу Orleans также создает копии для этого типа. Orleansне будет копировать типы или отдельные члены, помеченные с помощью .ImmutableAttribute Дополнительные сведения см. в разделе Сериализация неизменяемых типов в Orleans.
Рекомендации по сериализации
✅Дайте псевдонимы типов с помощью атрибута
[Alias("my-type")]
. Типы с псевдонимами можно переименовать без критической совместимости.❌Не изменяйте
record
регулярноеclass
или наоборот. Записи и классы не представлены одинаково, так как записи имеют основные члены конструктора в дополнение к обычным членам, поэтому они не взаимозаменяемы.❌Не добавляйте новые типы в существующую иерархию типов для сериализуемого типа. Не следует добавлять новый базовый класс в существующий тип. Вы можете безопасно добавить новый подкласс в существующий тип.
✅Замените использование SerializableAttribute GenerateSerializerAttribute соответствующими объявлениями.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
доshort
int
long
.- Можно сузить числовые типы элементов, но это приведет к исключению среды выполнения, если наблюдаемые значения не могут быть правильно представлены узким типом. Например,
int.MaxValue
не может быть представленоshort
полем, поэтому сужениеint
поляshort
может привести к исключению среды выполнения при обнаружении такого значения.
- Можно сузить числовые типы элементов, но это приведет к исключению среды выполнения, если наблюдаемые значения не могут быть правильно представлены узким типом. Например,
❌Не изменяйте подпись элемента числового типа. Например, не следует изменять тип члена с
uint
int
или на типint
uint
.
Сериализаторы хранилища зерна
Orleans включает в себя модель сохраняемости с поддержкой поставщика для зерна, доступ к которым осуществляется через State свойство или путем внедрения одного или нескольких IPersistentState<TState> значений в зерно. До Orleans версии 7.0 у каждого поставщика был другой механизм настройки сериализации. В Orleans версии 7.0 теперь существует интерфейс сериализатора состояния общего назначения, IGrainStorageSerializerкоторый обеспечивает согласованный способ настройки сериализации состояния для каждого поставщика. Поддерживаемые поставщики хранилища реализуют шаблон, который включает настройку IStorageProviderSerializerOptions.GrainStorageSerializer свойства в классе параметров поставщика, например:
- DynamoDBStorageOptions.GrainStorageSerializer
- AzureBlobStorageOptions.GrainStorageSerializer
- AzureTableStorageOptions.GrainStorageSerializer
- 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сериализатора задают его отдельно от множества других сторонних платформ сериализации: динамические типы или произвольный полиморфизм и удостоверение объекта.
Динамические типы и произвольный полиморфизм: Orleans не применяет ограничения на типы, которые могут передаваться в вызовах зерна и поддерживать динамический характер фактического типа данных. Это означает, что, например, если метод в интерфейсах зерна объявлен для принятия IDictionary , но во время выполнения отправитель проходит SortedDictionary<TKey,TValue>, получатель действительно получит
SortedDictionary
(хотя интерфейс статического контракта или зерна не указал это поведение).Сохранение удостоверения объекта: если один и тот же объект передает несколько типов в аргументах вызова зерна или косвенно указывает несколько раз из аргументов, Orleans сериализует его только один раз. На стороне получателя все ссылки будут восстановлены правильно, Orleans чтобы два указателя на один и тот же объект по-прежнему указывали на тот же объект после десериализации. Удостоверение объекта важно сохранить в таких сценариях, как показано ниже. Представьте себе, что зерно A отправляет словарь с 100 записями в зерно B, а 10 ключей в словаре указывают на тот же объект, obj, на стороне A. Без сохранения удостоверения объекта B получит словарь из 100 записей с этими 10 ключами, указывающими на 10 различных клонов obj. При сохранении удостоверения объекта словарь на стороне B выглядит точно так же, как на стороне A с этими 10 ключами, указывающими на один объект obj.
Приведенные выше два поведения предоставляются стандартным двоичным сериализатором .NET и поэтому важно для нас поддерживать это стандартное и знакомое поведение Orleans .
Созданные сериализаторы
Orleans использует следующие правила, чтобы решить, какие сериализаторы необходимо создать. Ниже приведены правила.
- Сканируйте все типы во всех сборках, ссылающихся на основную Orleans библиотеку.
- Из этих сборок создайте сериализаторы для типов, на которые напрямую ссылаются сигнатуры методов сигнатуры или сигнатуры класса состояний, или для любого типа, помеченного как SerializableAttribute.
- Кроме того, для создания сериализации можно указать произвольные типы интерфейса или реализации интерфейса или реализации, добавив KnownTypeAttribute KnownAssemblyAttribute атрибуты уровня сборки, чтобы сообщить генератору кода создавать сериализаторы для определенных типов или всех подходящих типов в сборке. Дополнительные сведения об атрибутах уровня сборки см. в разделе "Применение атрибутов на уровне сборки".
Резервная сериализация
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
быть опасной. Дополнительные сведения см. в руководстве по безопасности BinaryFormatter и руководстве по миграции BinaryFormatter.
Сериализация исключений
Исключения сериализуются с помощью резервного сериализатора. При использовании конфигурации по умолчанию используется резервный сериализатор, 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следующих целях:
- В качестве формата провода для передачи данных между зернами и клиентами во время выполнения.
- В качестве формата хранилища для сохранения долгоживующих данных для последующего извлечения.
Сериализаторы, созданные компанией Orleans , подходят для первой цели из-за их гибкости, производительности и универсальности. Они не так подходят для второй цели, так как они не являются явным образом терпимыми к версиям. Рекомендуется настроить терпимый к версиям сериализатор, например буферы протокола для постоянных данных. Буферы протокола поддерживаются из Orleans.Serialization.ProtobufSerializer
Microsoft.Orleans. Пакет NuGet OrleansGoogleUtils . Для обеспечения допустимости версий следует использовать рекомендации по выбору конкретного сериализатора. Сторонние сериализаторы можно настроить с помощью SerializationProviders
свойства конфигурации, как описано выше.