DtoKit
Библиотека DtoKit
предназначена для предотвращения зависимости модели и представления приложения, как серверной, так и клиентской частей,
от конкретной реализации классов объектов предметной области. В то же время, её использование позволит избежать переноса данных между уровнями приложения
с помощью Data Transfer Objects (DTO) или Plain Old CLR Object (POCO). Сказанное относится к построению объектов предметной области, их сериализации в JSON и
десериализации из JSON для взаимодействия между клиентом и сервером.
Дерево класса
Деревом класса из предметной области будем называть связный ациклический граф (собственно, дерево), корнем которого является рассматриваемый класс, узлами - агрегированные классы из предметной области, листьями - открытые для записи свойства.
Рассмотрим пример из предметной области, связанной с расписанием движения морских судов.
Для краткости приведём только основные свойства сущностей, хотя они могут содержать и методы, но в данном контексте это неважно.
Для класса ShipCall
имеем следующее дерево (имена узлов опущены для простоты восприятия):

Как видим, дерево класса может быть бесконечным.
Дерево интерфейса
Теперь мы можем разработать интерфейсы только для чтения, которые покрывают все свойства в наших классах предметной области и в рамках предметной области, то есть ключи БД нигде фигурировать не будут. Заставим классы их реализовать явно, если потребуется, это не добавит никакой логики. Мы будем редактировать классы, но это ни на что не повлияет, так как новые свойства будут лишь "псевдонимами" свойств класса.
Также сразу учтём, что нам нужно будет неявно всё же использовать ключевые свойства на уровнях загрузки и передачи, хотя бы для запросов клиента к конкретным объектам на сервере
(дальше увидим, что не только для этого), поэтому пометим такие свойства специальным атрибутом [Key]
.
Вот что получилось:
Очевидно, по интуитивно понятным образом модифицированному определению для дерева классов строится дерево интерфейса.
Например, дерево интерфейса IShipCall
выглядит так (имена узлов опущены для простоты восприятия):

Как видим, дерево интерфейса тоже может быть бесконечным.
Сужающие интерфейсы
Предположим, мы хотим вывести список судозаходов клиенту в виде таблицы. Целиком нам объекты не понадобятся, но и не хочется делать отдельный класс для строки таблицы, так как его "физический" смысл будет неясным.
Пусть нам нужно вывести в таблицу название линии, название парохода, название порта прибытия/отхода, номер вояжа, время прибытия/убытия, дополнительную информацию. Тогда мы делаем новое дерево интерфейсов, которое содержит только эти данные:
Например, дерево интерфейса IShipCallForList
выглядит так (имена узлов опущены для простоты восприятия):

Также в классе ShipCall
у нас есть свойство AdditionalInfo
, которое загружается долго из-за отдельного запроса
к БД. Поэтому мы хотим сначала выводить пользователю таблицу, а после этого постепенно обновлять эти ячейки.
Заведём ещё один интерфейс:
Дерево интерфейса IShipCallAdditionInfo
выглядит так (имена узлов опущены для простоты восприятия):

AddDtoKit(...)
Этот метод должен быть вызван во время конфигурирования механизма внедрения зависимости.
Через параметр IServiceCollection services
нужно передать коллекцию сервисов, предоставляемую хостом,
а параметр Action
должен содержать метод, который регистриует все интерфейсы,
которые введены для классов предметной области, в коллекции сервисов, предоставляемой библиотекой DtoKit
.
В нашем случае это будет выглядеть так:
Следует иметь в виду, что в данном контексте время жизни может быть только ServiceLifetime.Transient
, другие вызовут исключительную ситуацию.
DtoBuilder
Класс DtoBuilder
предназначен для загрузки объектов из хранилища, например, БД.
Имеет особенности:
- загружает только ключевые свойства и свойства, соответствующие дереву запрошенного интерфейса
- не дублирует уже загруженные объекты с тем же деревом
Экземпляр DtoBuilder
нужно получить через механизм внедрения зависимости, он уже зарегистрирован там при вызове AddDtoKit(...)
:
Затем использовать одним из двух способов (T - запрашиваемый интерфейс):
- Подписаться на событие
ValueRequest
и вызватьBuild<T>()
- Вызвать
Build<T>(object helper)
, гдеhelper
- специальный объект
В обоих случаях метод вернёт построенный объект. Если нужно загрузить уже существующий объект, его нужно присвоить свойству Target
.
В противном случае объект будет получен через внедрение зависимости.
Build<T>()
и BuildOfType(Type type)
При использовании этого способа нужно подписаться на событие ValueRequest
:
При вызове метода Build<T>()
происходит обход дерева запрошенного интерфейса в ширину и в каждом узле и листе
вызывается обработчик события, которому передаётся аргумент типа ValueRequestEventArgs
.
ValueRequestEventArgs
Члены:
public Type RootType { get; }
|
тип запрошенного интерфейса |
public string Path { get; }
|
путь от корня дерева класса - имена свойств через / |
public bool IsNullable { get; }
|
указывает на возможность присвоить свойству значение null . Основывается на наличии ? в типе свойства;
DtoBuilder запрещает присваивать null , если значение равно false
|
public bool IsLeaf { get; }
|
указывает, что текущий узел в дереве интерфейса является листом. Это означает, что его тип не был зарегистрирован при вызове
AddDtoKit(...)
|
public Type NominalType { get; }
|
тип свойства текущего узла в дереве интерфейса |
public object? Value { get; set; }
|
возвращает текущее значение свойства и служит для присвоения нового значения. В случае узла объект уже предоставлен |
public bool IsCommited { get; set; }
|
при присвоении значения true узлы и листья поддерева с корнем в данном узле пропускаются.
|
В случае узла можно никаких действий не предпринимать, так как объект уже присвоен. В случае листа необходимо явно присвоить значение свойству Value
,
иначе возникнет исключение.
Build<T>(object helper)
и BuildOfType(Type type, object helper)
helper
- объект произвольного класса, содержащий публичные методы, помеченные особыми атрибутами и имеющие особую метрику.
[Startup]
|
необязательный метод, который в случае наличия вызывается перед началом построения объекта |
[Shutdown]
|
необязательный метод, который в случае наличия вызывается после окончания построения объекта |
[Before]
|
необязательный метод, который в случае наличия вызывается перед запросом значения каждого узла или листа |
[After]
|
необязательный метод, который в случае наличия вызывается после запроса значения каждого узла или листа |
[Path("/...")]
|
метод, обязательный для листьев и необязательный для узлов. Если определён для узла, должен возвращать значение аргумента value , если
остаётся предоставленный объект, либо null , либо ссылку на другой объект, по решению разработчика.
Если не определён для листа, вызывается исключение. Один метод может иметь несколько атрибутов [Path("/...")] . Все пути должны быть различны в
пределах класса
|
Параметры для методов, отмеченных [Before]
, [After]
, [Path("/...")]
:
string path
|
путь от корня дерева класса |
Type type
|
тип свойства текущего узла в дереве интерфейса |
object? value
|
исходное значение свойства текущего узла. Для листа имеет значение default . Следует иметь в виду, что если свойство-лист имеет какое-то значение
по умолчаниию при создании объекта, на него рассчитывать нельзя, так как оно теряется.
|
bool isLeaf
|
указывает, что текущий узел в дереве интерфейса является листом. Это означает, что его тип не был зарегистрирован при вызове
AddDtoKit(...)
|
bool isNullable
|
указывает на возможность присвоить свойству значение null . Основывается на наличии ? в типе свойства;
DtoBuilder запрещает присваивать null , если значение равно false
|
ref bool isCommited
|
при присвоении значения true узлы и листья поддерева с корнем в данном узле пропускаются.
|
Важно! Использование бесконечных деревьев интерфейсов ограничено! Если на пути от узла к корню встречается узел того же типа, то это ошибка, кроме случая, когда эти узлы - концы одного ребра. В этом случае ошибки нет, но загружаются только ключевые свойства.
Например, можно написать программу, которая выведет все пути:
/
/ID_LINE
/ID_SHIPCALL
/AdditionalInfo
/Arrival
/Departure
/Voyage
/Port
/Port/ID_PORT
/Port/Name
/PrevCall
/PrevCall/ID_LINE
/PrevCall/ID_SHIPCALL
/Route
/Route/ID_LINE
/Route/ID_ROUTE
/Route/Line
/Route/Line/ID_LINE
/Route/Line/Name
/Route/Vessel
/Route/Vessel/ID_VESSEL
/Route/Vessel/Brutto
/Route/Vessel/CallSign
/Route/Vessel/Height
/Route/Vessel/Length
/Route/Vessel/Name
/Route/Vessel/Netto
/Route/Vessel/Width
/Route/Vessel/Port
/Route/Vessel/Port/ID_PORT
/Route/Vessel/Port/Name
Видим, что для PrevCall
запрашиваются только ключи.
DtoJsonConverterFactory
Класс DtoJsonConverterFactory
является фабрикой кастомных Json конвертеров для работы с интерфейсами, зарегистрированными через
AddDtoKit(...)
, а также с их коллекциями стандартными средствами из пространства имён System.Text.Json.*
.
Имеет особенности:
- при сериализации записывает в результирующий Json только ключевые свойства и свойства, соответствующие дереву запрошенного интерфейса
- по умолчанию включен режим сериализации , при котором уже сериализованные объекты записываются в Json только ключевыми свойствами
- имеет режим сериализации при котором уже все объекты записываются в Json только ключевыми свойствами. Это удобно для запросов к серверу
- не дублирует объекты при десериализации
-
Значения перечислимых типов (
enum
) автоматически сериализует/десериализует по именам - В зависимости от настроек может переписывать, дополнять и обновлять целевую коллекцию объектов при десериализации Json-массива
Экземпляр DtoJsonConverterFactory
нужно получить через механизм внедрения зависимости, он уже зарегистрирован там при вызове AddDtoKit(...)
,
создать экземпляр System.Text.Json.JsonSerializerOptions
, добавить фабрику в его конвертеры, а затем использовать обычный механизм, подставляя аргумент JsonSerializerOptions
:
Сериализация
Свойства, используемые только при сериализации:
public KeysProcessing KeysProcessing { get; set; }
|
Указывает, как поступать с ключевыми свойствами:
|
||||||
public bool WithMagic
|
при присвоении значения true в каждый Json-объект добавляется специальное поле {"$magic": "applied", ...} .
Это удобно для визуальнй проверки, что все нужные объекты сериализуются именно с помощью DtoJsonConverterFactory
|
Десериализация
Свойства, используемые только при десериализации:
public bool UseEndOfDataNull { get; set; }
|
при десериализации Json-массива верхнего уровня указывает на возможно частичное содержимое. Последний элемент очередного массива,
равный null означает конец коллекции и устанавливает IsEndOfData = true .
Тот факт, что передача коллекции будет происходить частями сообщается клиенту отдельно, например, через http-заголовок
|
public bool IsEndOfData { get; }
|
значение true означает, что UseEndOfDataNull == true и пришёл null , то есть передача коллекции по частям
завершена. Тот факт, что передача коллекции будет происходить частями сообщается клиенту отдельно, например, через http-заголовок
|
public object? Target { get; set; }
|
при присвоении объекта данному свойству Json десериализуется в него, вместо создания нового объекта. Не рекомендуется читать это свойство, так как в процессе десериализации оно меняется непредсказуемо с точки зрения наблюдателя |
Также при десериализации Json-массива верхнего уровня можно использовать специальные фиктивные типы для управления заполнением целевой коллекции объектов.
В этом случае целевая коллекция обязательно должна существовать и перед десериализацией её нужно присвоить свойству Target
.
Фиктивный тип указывается в качестве параметра типа при вызове JsonSerializer.Deserialize<...>(...)
. Возвращаемое значение такого вызова
следует игнорировать, так как это просто заглушка, а фактически заполняется коллекция, предварительно присвоенная свойству Target
. Смысл использования
данной техники в том, чтобы работать на клиенте с ObservableCollection
, связанной с интерфейсом пользователя.
RewritableListStub<T>
|
заполняет целевую коллекцию с начала, повторно использую уже содержащиеся в ней объекты, удаляя в конце лишние или добавляя недостающие |
AppendableListStub<T>
|
добавляет объекты в конец целевой коллекции. Удобно для заполнения частями. |
UpdateableListStub<T>
|
Обновляет объекты целевой коллекции, находя их по ключевым свойствам.
Например, в в примере с дополнительной информацией по судозаходу, мы сначала загружаем судозаходы с пустой дополнительной информацией, а затем загружаем только дополнительную информацию, вписывая её в нужные строки. Так как дополнительная информация по легенде вычисляется медленно, пользователь не ждёт, а получает таблицу быстро, а потом видит и дополнительную информацию. |