# Как использовать

Сериализация — процесс перевода структуры данных в последовательность байтов. Мы можем производить сериализацию как простых типов, так и моделей объектов реального мира, которые описываются в виде классов, называются бизнес-объектами или сущностями, и отвечают следующим условиям:

  • сериализуеммый класс содержит открытый конструктор без параметров;
  • каждому классу бизнес-объекта сопоставлен BusinessObjectId — уникальный целочисленный идентификатора типа ulong;
  • сериализуемые свойства класса являются публичными и имеют сопоставление с TagId — уникальным целочисленным идентификатором свойства в рамках класса бизнес-объекта (имеет тип ulong).

Сопоставления BusinessObjectId и TagId осуществляются путём аннотация данных с помощью атрибутов или средствами Fluent API.

# Бизнес-объекты и аннотация данных с помощью атрибутов

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

Для этого подхода определены следующие атрибуты:

  • BusinessObjectIdAttribute — задаёт значение уникального идентификатора класса бизнес-объекта (используется при сериализации, для десериализации будет проигнорировано);

  • TagIdAttribute — задаёт значение уникального идентификатора свойства в рамках класса;

  • DateTimeEncodingRuleAttribute — для свойств с типами DateTime и DateTimeOffset позволяет определить правило кодирования (DateOnly, DateTime, DateTimeOffset);

  • EnumAsStringAttribute — для свойств с типом перечисление (enum) задает правило кодирования в виде строки в кодировке UTF-8 (по-умолчанию используется целочисленный базовый тип данных);

  • StringEncodingRuleAttribute — для свойств с типом строка (string) задает кодировку, в которую будет преобразован текст перед его кодированием (в случае ошибки преобразования и включенном режиме SafeMode данные будут сохранены в кодировке UTF-8).

Выберем в качестве объекта реального мира пропуск для доступа к объектам социальной инфраструктуры и опишем его модель в виде класса ExtraCityPass с использованием атрибутов.

[BusinessObjectId(1)]
public class ExtraCityPass1
{
    [TagId(1)]
    public int Id { get; set; }

    [TagId(2)]
    [EnumAsString]
    public CountryCode1 Nationality { get; set; }

    [TagId(3)]
    [StringEncodingRule(StringEncodingMode.Numeric)]
    public string PassportNumber { get; set; }

    [TagId(4)]
    [StringEncodingRule(StringEncodingMode.Predefined, SafeMode = true)]
    public string FullName { get; set; }

    [TagId(5)]
    public Gender1 Sex { get; set; }

    [TagId(6)]
    [DateTimeEncodingRule(DateTimeEncodingMode.DateOnly)]
    public DateTime DateOfBirth { get; set; }

    [TagId(7)]
    public string Address { get; set; }

    [TagId(8)]
    [StringEncodingRule(StringEncodingMode.Numeric)]
    public string MobileId { get; set; }

    [TagId(9)]
    public string Email { get; set; }

    [TagId(10)]
    public bool IsVactinated { get; set; }

    public string Authority { get; set; }

    [TagId(12)]
    [DateTimeEncodingRule(DateTimeEncodingMode.DateTimeOffset)]
    public DateTime NotValidAfter { get; set; }

    [TagId(63)]
    public byte[] Signature { get; set; }
}


// Dependent Elements
public enum Gender1 : byte
{
    None = 0,
    Male,
    Female
}

public enum CountryCode1
{
    None = 0,
    GBR,
    CHN,
    KAZ,
    RUS,
    UKR,
    USA,
}

# Бизнес-объекты и Fluent API

Не всегда мы можем воспользоваться методом аннотации данных с помощью атрибутов и применить его к требуемому классу бизнес-объекта, как правило это обусловлено следующими факторами:

  • нам необходимо работать с уже существующим классом из чужой сборки (например, другого разработчика);
  • требуется динамическое сопоставление BusinessObjectId и TagId;
  • требования политики информационной безопасности предприятия.

В этом случае мы оперируем классом бизнес-объекта который, как минимум, имеет открытый конструктор без параметров и публичные свойства:

public class ExtraCityPass2
{
    public int Id { get; set; }
    public CountryCode2 Nationality { get; set; }
    public string PassportNumber { get; set; }
    public string FullName { get; set; }
    public Gender2 Sex { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string Address { get; set; }
    public string MobileId { get; set; }
    public string Email { get; set; }
    public bool IsVactinated { get; set; }
    public string Authority { get; set; }
    public DateTime NotValidAfter { get; set; }
    public byte[] Signature { get; set; }
}


// Dependent Elements
public enum Gender2 : byte
{
    None = 0,
    Male,
    Female
}

public enum CountryCode2
{
    None = 0,
    GBR,
    CHN,
    KAZ,
    RUS,
    UKR,
    USA,
}

Для работы с классом бизнес-объекта, не имеющим аннотацию данных, мы должны подготовить декларативную информацию с помощью классов BusinessObjectConfiguration и PropertyConfiguration, и передать её экземпляру сериализатора используя метод AddBusinessObjectConfiguration().

С помощью метода GetProperty() класса BusinessObjectConfiguration мы получаем доступ к цепочке методов конфигурирования свойства класса, аналогичных по функциональности атрибутам аннотации данных:

  • SetTagId();
  • SetDateTimeEncodingRule();
  • SetEnumAsString();
  • SetStringEncodingRule().

Подготовим и применим ту же декларативную информацию для нашего класса ExtraCityPass альтернативным способом:

var conf2 = new BusinessObjectConfiguration<ExtraCityPass2>(id: 2);

conf2.GetProperty(nameof(ExtraCityPass2.Id))
    .SetTagId(1)
    .SetStringEncodingRule(StringEncodingMode.Numeric);

conf2.GetProperty(nameof(ExtraCityPass2.Nationality))
    .SetTagId(2)
    .SetEnumAsString();

conf2.GetProperty(nameof(ExtraCityPass2.PassportNumber))
    .SetTagId(3)
    .SetStringEncodingRule(StringEncodingMode.Numeric);

conf2.GetProperty(nameof(ExtraCityPass2.FullName))
    .SetTagId(4)
    .SetStringEncodingRule(StringEncodingMode.Predefined, SafeMode = true);

conf2.GetProperty(nameof(ExtraCityPass2.Sex))
    .SetTagId(5);

conf2.GetProperty(nameof(ExtraCityPass2.DateOfBirth))
    .SetTagId(6)
    .SetDateTimeEncodingRule(DateTimeEncodingMode.DateOnly);

conf2.GetProperty(nameof(ExtraCityPass2.Address))
    .SetTagId(7);

conf2.GetProperty(nameof(ExtraCityPass2.MobileId))
    .SetTagId(8)
    .SetStringEncodingRule(StringEncodingMode.Numeric);

conf2.GetProperty(nameof(ExtraCityPass2.Email))
    .SetTagId(9);

conf2.GetProperty(nameof(ExtraCityPass2.IsVactinated))
    .SetTagId(10);

conf2.GetProperty(nameof(ExtraCityPass2.NotValidAfter))
    .SetTagId(12)
    .SetDateTimeEncodingRule(DateTimeEncodingMode.DateTimeOffset);

conf2.GetProperty(nameof(ExtraCityPass2.Signature))
    .SetTagId(63);

conf2.SyncTags();
//serializer.AddBusinessObjectConfiguration(conf2);

С помощью метода SyncTags() класса BusinessObjectConfiguration осуществляется синхронизация уникальных идентификаторов свойств, возможно это выглядит как "костыль" и в дальнейшем эта функциональность будет изменена.

# Пример сериализации и десериализации бизнес-объекта

Продемонстрируем работу бинарного сериализатора на примере уже определенных классов ExtraCityPass1 и ExtraCityPass2, а также подготовленной декларативной информации, содержащейся в переменной conf2.

Инициализируем экземпляр класса ExtraCityPass1 и наполним его демонстрационными данными:

var obj = new ExtraCityPass1
{
    Id = 7714377,
    Nationality = CountryCode.USA,
    PassportNumber = "2571459",
    FullName = "Elvis Aaron Presley",
    Sex = Gender.Male,
    DateOfBirth = new DateTime(1935, 1, 8),
    Address = "3764 Elvis Presley Boulevard, Memphis, TN 38116",
    MobileId = "19013323322",
    Email = "elvis@example.com",
    IsVactinated = false,
    Authority = "Tennessee DMV",
    NotValidAfter = new DateTime(1977, 8, 16),
};

# Сериализация

var serializer = new XyloCode.BusinessData.Serializer();
serializer.DefaultCharacterEncoding = Encoding.Latin1;

byte[] data = serializer.Encode(obj);
Console.WriteLine(data.Length);
> 153

Данные из переменной data будут использованы для последующих примеров.

# Десериализация в класс бизнес-объекта с аннотацией данных с помощью атрибутов

var serializer = new XyloCode.BusinessData.Serializer();
serializer.DefaultCharacterEncoding = Encoding.Latin1;

ExtraCityPass1 newObj = serializer.Decode<ExtraCityPass1>(data);

# Десериализация в произвольный класс с предварительным применением декларативной информации

var serializer = new XyloCode.BusinessData.Serializer();
serializer.DefaultCharacterEncoding = Encoding.Latin1;
serializer.AddBusinessObjectConfiguration(conf2);

ExtraCityPass2 newObj = serializer.Decode<ExtraCityPass2>(data);

# Декодирование сериализованных данных в случае, если структура объекта неизвестна

var serializer = new XyloCode.BusinessData.Serializer();
serializer.DefaultCharacterEncoding = Encoding.Latin1;

string verbose = serializer.Print(data);
Console.WriteLine(verbose);
> BusinessData v1
> ********************************************
> (BusinessObjectId = 0)
> [1] => (Int32Var) 7714377
> [2] => (UTF8) USA
> [3] => (StringNumber) 2571459
> [4] => (String) Elvis Aaron Presley
> [5] => (Enum : Byte) 1
> [6] => (DateOnly) 08.01.1935
> [7] => (String) 3764 Elvis Presley Boulevard, Memphis, TN 38116
> [8] => (StringNumber) 19013323322
> [9] => (String) elvis@example.com
> [10] => (BooleanFalse) False
> [12] => (DateTimeOffset) 16.08.1977 0:00:00 -05:00
> ********************************************

# Специализированные кодировки строк

Бинарный сериализатор позволяет использовать специализированные кодировки (производные классы от абстрактного System.Text.Encoding) для преобразования данных публичных свойств типа string, что особенно актуально для языков, алфавиты которых не покрываются ASCII-символами и содержат до 40..45 знаков. Кроме того, использование специализированных кодировок может рассматриваться как средство защиты информации.

Способ кодирования строки может быть задан непосредственно для конкретного публичного строкового свойства с помощью параметра mode:

  • атрибута аннотации данных StringEncodingRuleAttribute;
  • метода настройки свойства SetStringEncodingRule() Fluent API.

Параметр mode является перечислением типа StringEncodingMode и может принимать следующие значения:

  • UTF8 — использовать кодировку UTF-8;
  • UTF16LE — использовать кодировку UTF-16 с обратным порядком байтов;
  • CodePage — использовать кодировку с кодом, указанным в именованом аргументе CodePage атрибута аннотации данных или метода настройки свойства Fluent API;
  • Numeric — использовать 4-х битную кодировку с поддержкой символов: 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, *, -, ., /, :, ' ' (пробел);
  • Predefined — использовать кодировку, экземпляр класса которой назначен свойству DefaultCharacterEncoding бинарного сериализатора.

# Явное указание кода кодировки

При режиме кодирования StringEncodingMode.CodePage сериализатор добавляет код используемой кодировки к каждому строковому свойству, которое кодируется этим способом. В случае, если бизнес-объект содержит множество таких свойств, возможно, имеет смысл рассмотреть способ использования с использованием предопределенной кодировки.

Пример настройки свойства методом аннотации данных с помощью атрибутов:

[StringEncodingRule(StringEncodingMode.CodePage, CodePage = 866)]
public string FullName { get; set; }

Пример настройки свойства средствами Fluent API:

conf.GetProperty("FullName")
    .SetStringEncodingRule(StringEncodingMode.CodePage, CodePage = 866);

# Использование предопределенной кодировки

Этот способ может являться наиболее экономичным с точки зрения размера сериализованного объекта. В то же время, декодировать строковые данные, сохраненные этим способом, без наличия библиотеки с необходимой кодировкой будет весьма затруднительно.

Кодирование и декодирование свойств строкового типа в режиме StringEncodingMode.Predefined предполагает, что сериализатору уже известен способ кодирования строк в этом режиме, причём он является единственным, например:

var serializer = new XyloCode.BusinessData.Serializer();
serializer.DefaultCharacterEncoding = new RussianSixBitEncoding();

Настройка свойства методом аннотации данных с помощью атрибутов:

[StringEncodingRule(StringEncodingMode.Predefined)]
public string FullName { get; set; }

Настройка свойства средствами Fluent API:

conf.GetProperty("FullName")
    .SetStringEncodingRule(StringEncodingMode.Predefined);