Должен ли DTO содержать аннотации данных?

127
25 мая 2019, 04:10

Имеется приложение ASP.Net Core 2.1 (MVC) с EF Core (ASP.Net Boilerplate Zero Core) и с Automapper. Приложение имеет классы сущностей, на основе которых EF Core создаёт миграции для БД и описывает SQL типы, а также DTO классы. Для описания типа в БД и создания миграций EF Core использует аннотации данных классов сущностей. Проблема заключается в том, что приходиться использовать аннотации данных и в классах сущностей, и в DTO для проверки вводимых данных. Аннотации добавляются к DTO т.к. при возникновении ошибки маппинга DTO<->Entity возникающее исключение не содержит информации о вводимых данных и их корректности, а просто выкидывается исключение automapper'а о невозможности маппинга. В данный момент в классах сущностей добавляются константы со значениями для аннотаций данных, что позволяет использовать их и в сущностях, и в DTO.

namespace Namespace.Persons {
    public class Person : Entity<Guid> {
        public const int FirstNameMaxLength = 50;
        public const int LastNameMaxLength = 50;
        public Person() { }
        [Required]
        [StringLength(FirstNameMaxLength)]
        public string FirstName { get; set; }
        [Required]
        [StringLength(LastNameMaxLength)]
        public string LastName { get; set; }
    }
}
namespace Namespace.Persons.Dto {
    [AutoMapFrom(typeof(Person))]
    public class PersonDto : EntityDto<Guid> {
        [Required]
        [StringLength(Person.FirstNameMaxLength)]
        public string FirstName { get; set; }
        [Required]
        [StringLength(Person.LastNameMaxLength)]
        public string LastName { get; set; }
    }
}

В связи с чем возникает вопрос: а не является ли это костылём, который в будущем обернётся проблемами, так как приходиться следить за соответствием аннотаций в двух различных местах? Имеется ли возможность "синхронизации" описания DTO на основе классов моделей базы данных?

Answer 1

Отличный вопрос, сам много думаю над аналогичными вещами.

Для себя пришёл на текущий момент к следующему варианту ответа. Мы с вами осознанно выбрали такие архитектуры приложения ("слоями"), которые нам обещали гибкость и удобство при расширении проекта по сравнению с более простыми архитектурами, которые дают быструю скорость на старте но проигрывают при долгосрочной поддержке и развитии проекта.

За эту гибкость и удобство мы и платим вполне осознанную цену: тем, что каждый раз делаем какие-то вещи, которые кажутся немного лишними и избыточными. Вы указываете на то, что у вас атрибуты повторяются на разных слоях приложения — да, верно. Однако представьте, что в какой-то момент времени вам вдруг придётся сделать их слегка отличающимися. Если бы вы использовали один класс на всё, как справедливо предложил @Qwertiy — то вам было бы гораздо труднее, чем в текущей архитектуре справиться с подобной проблемой, причём потенциальные решения станут именно архитектурным костылём, когда вместо честного "тут уже пора делать отдельные классы" вы будете вынуждены ставить дополнительные атрибуты на тот же класс, так как ранее написанный код обладает значительной инерцией и не позволит вам малой ценой перейти от одной архитектуры к другой.

Тут правда возникает один совершенно неудобный вопрос: а не случится ли так, что все атрибуты будут всегда строго одинаковы на разных слоях приложения, даже в теории? Но сначала небольшое отступление.

Ваша архитектурная проблема с автомаппером легко маскируется: просто не используйте автомаппер. ;) Вот пример приложения из выступления — там люди пишут маппинг руками и ничего, никто не умер. Не буду сильно разжигать по поводу не/использования маппера, для себя я не увидел разницы по времени между написанием маппинга явно или с использованием автомаппера, а вот с тем, что использование автомаппера приводит к долгому дебагу с крепкими матюками я уже сталкивался и пока не хочу. (Да-да, я возможно не очень хорошо умею готовить кошек)

Ну, "как маскируется" я упомянул только для того, чтобы указать в каком аспекте я сталкивался с этой проблемой. А у меня эта проблема выползает тогда, когда у меня атрибуты используются для валидации модели. Т.е. у меня подобные классы имеют суффикс не Dto, а ViewModel или Model.

В любом случае, попробуем посмотреть на то, где нам нужны одни и те же атрибуты и для чего:

  • первый раз мы их описываем в качестве доменных сущностей, но используем в качестве описания модели базы данных (если не пользуемся DatabaseFirst - и это ещё один шанс замаскировать проблему дублирования) совместно с FluentApi
  • второй раз мы описываем сходные атрибуты для описания валидации пришедших данных с клиента в GET/POST запросах
  • третий раз мы описываем сходные атрибуты для описания маппинга Entity < — > Dto/ViewModel

Скажу так. Можно легко уменьшить количество мест, где мы описываем атрибуты до двух, но вот уменьшение до одного места уже [на мой взгляд] невозможно без архитектурных компромиссов. Тут мы упрёмся в один религиозный вопрос — можно ли не создавать классы для ViewModel и использовать доменные сущности, лично я сторонник написания ViewModel отдельно.

Так вот, возвращаясь к неудобному вопросу, который я выше озвучил: а понадобится ли вообще сделать подобные атрибуты разными в теории и на практике? Лично я пока не уверен, что знаю окончательную редакцию ответа, но вот сходу я подобных примеров в практике не могу вспомнить (если кто хочет дать пример — с радостью почитаю в комментариях). Тем не менее, для меня принципиальна важна именно архитектурная чистота и тут ответ более ясен: это дублирование хоть и отражает какие-то глубинные инварианты предметной сущности, но фактически служит разным целям (консистентность БД и валидация клиентских данных), поэтому вполне возможно не обращать внимание на подобное дублирование. Ну или более определённо: я (пока?) не знаю способа выразить в семантике c# подобные глубинные инварианты менее многословно, чтобы не потерять гибкость.

В связи с чем возникает вопрос: а не является ли это костылём, который в будущем обернётся проблемами, так как приходиться следить за соответствием аннотаций в двух различных местах? Имеется ли возможность "синхронизации" описания DTO на основе классов моделей базы данных?

Итого, моя версия ответа:

  • DDD, увы, многословен, но нет, это совершенно точно не костыль, а явный и полезный архитектурный приём.
  • я знаю только одну проблему: когда у вас проверки разойдутся (например, при валидации максимальная строки 100, а в БД — лишь 50), я смягчаю это тем, что выношу подобные константы в самую глубину доменной логики — константами в классе модели.
  • есть ли возможность "синхронизации" описания — не знаю, но подозреваю, что все способы будут приводить к каким-то серьёзным ухудшениям текущей архитектуры по другим параметрам

Ну и в заключение немного оффтопика. Я раньше любил программирование на атрибутах, но всё чаще сталкиваюсь с ограничениями подобного подхода. Например, майкрософт придумала fluent api в довесок к аннотациям, так как не всё можно просто описать атрибутами. Плюс я вижу преимущества в fluent validation (отсылаю к тому же видео).

В общем-то всё. Не могу сказать, что это полноценный ответ на ваш вопрос — скорее больше пересказ моего опыта, поэтому если кому есть что дополнить из своей практики — буду рад почитать.

READ ALSO
&ldquo;Занять&rdquo; файл на время

“Занять” файл на время

Разрабатываю приложение, которое участвует в цепочке перемещения файловТо есть: как только в папке 1 появляется файл, мое приложение его...

173
Глобальный хук клавиш

Глобальный хук клавиш

По какой-то причине хук обрабатывается, но ничего не происходит, приложение просто напросто закрывается без каких-либо ошибок

138
Winforms Ribbon интерфейс под Windows 10?

Winforms Ribbon интерфейс под Windows 10?

Подскажите, пожалуйста, существует ли под Windows 10 бесплатная библиотека для C# WinForms для отображения Ribbon интерфейса?

151