Страницы

Поиск по вопросам

четверг, 9 января 2020 г.

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

#c_sharp #aspnet_mvc #архитектура #automapper #aspnetboilerplate


Имеется приложение 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 {

        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 {

        [Required]
        [StringLength(Person.FirstNameMaxLength)]
        public string FirstName { get; set; }

        [Required]
        [StringLength(Person.LastNameMaxLength)]
        public string LastName { get; set; }
    }
}


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


Ответы

Ответ 1



Отличный вопрос, сам много думаю над аналогичными вещами. Для себя пришёл на текущий момент к следующему варианту ответа. Мы с вами осознанно выбрали такие архитектуры приложения ("слоями"), которые нам обещали гибкость и удобство при расширении проекта по сравнению с более простыми архитектурами, которые дают быструю скорость на старте но проигрывают при долгосрочной поддержке и развитии проекта. За эту гибкость и удобство мы и платим вполне осознанную цену: тем, что каждый раз делаем какие-то вещи, которые кажутся немного лишними и избыточными. Вы указываете на то, что у вас атрибуты повторяются на разных слоях приложения — да, верно. Однако представьте, что в какой-то момент времени вам вдруг придётся сделать их слегка отличающимися. Если бы вы использовали один класс на всё, как справедливо предложил @Qwertiy — то вам было бы гораздо труднее, чем в текущей архитектуре справиться с подобной проблемой, причём потенциальные решения станут именно архитектурным костылём, когда вместо честного "тут уже пора делать отдельные классы" вы будете вынуждены ставить дополнительные атрибуты на тот же класс, так как ранее написанный код обладает значительной инерцией и не позволит вам малой ценой перейти от одной архитектуры к другой. Тут правда возникает один совершенно неудобный вопрос: а не случится ли так, что все атрибуты будут всегда строго одинаковы на разных слоях приложения, даже в теории? Но сначала небольшое отступление. Ваша архитектурная проблема с автомаппером легко маскируется: просто не используйте автомаппер. ;) Вот пример приложения из выступления — там люди пишут маппинг руками и ничего, никто не умер. Не буду сильно разжигать по поводу не/использования маппера, для себя я не увидел разницы по времени между написанием маппинга явно или с использованием автомаппера, а вот с тем, что использование автомаппера приводит к долгому дебагу с крепкими матюками я уже сталкивался и пока не хочу. (Да-да, я возможно не очень хорошо умею готовить кошек) Ну, "как маскируется" я упомянул только для того, чтобы указать в каком аспекте я сталкивался с этой проблемой. А у меня эта проблема выползает тогда, когда у меня атрибуты используются для валидации модели. Т.е. у меня подобные классы имеют суффикс не Dto, а ViewModel или Model. В любом случае, попробуем посмотреть на то, где нам нужны одни и те же атрибуты и для чего: первый раз мы их описываем в качестве доменных сущностей, но используем в качестве описания модели базы данных (если не пользуемся DatabaseFirst - и это ещё один шанс замаскировать проблему дублирования) совместно с FluentApi второй раз мы описываем сходные атрибуты для описания валидации пришедших данных с клиента в GET/POST запросах третий раз мы описываем сходные атрибуты для описания маппинга Entity < — > Dto/ViewModel Скажу так. Можно легко уменьшить количество мест, где мы описываем атрибуты до двух, но вот уменьшение до одного места уже [на мой взгляд] невозможно без архитектурных компромиссов. Тут мы упрёмся в один религиозный вопрос — можно ли не создавать классы для ViewModel и использовать доменные сущности, лично я сторонник написания ViewModel отдельно. Так вот, возвращаясь к неудобному вопросу, который я выше озвучил: а понадобится ли вообще сделать подобные атрибуты разными в теории и на практике? Лично я пока не уверен, что знаю окончательную редакцию ответа, но вот сходу я подобных примеров в практике не могу вспомнить (если кто хочет дать пример — с радостью почитаю в комментариях). Тем не менее, для меня принципиальна важна именно архитектурная чистота и тут ответ более ясен: это дублирование хоть и отражает какие-то глубинные инварианты предметной сущности, но фактически служит разным целям (консистентность БД и валидация клиентских данных), поэтому вполне возможно не обращать внимание на подобное дублирование. Ну или более определённо: я (пока?) не знаю способа выразить в семантике c# подобные глубинные инварианты менее многословно, чтобы не потерять гибкость. В связи с чем возникает вопрос: а не является ли это костылём, который в будущем обернётся проблемами, так как приходиться следить за соответствием аннотаций в двух различных местах? Имеется ли возможность "синхронизации" описания DTO на основе классов моделей базы данных? Итого, моя версия ответа: DDD, увы, многословен, но нет, это совершенно точно не костыль, а явный и полезный архитектурный приём. я знаю только одну проблему: когда у вас проверки разойдутся (например, при валидации максимальная строки 100, а в БД — лишь 50), я смягчаю это тем, что выношу подобные константы в самую глубину доменной логики — константами в классе модели. есть ли возможность "синхронизации" описания — не знаю, но подозреваю, что все способы будут приводить к каким-то серьёзным ухудшениям текущей архитектуры по другим параметрам Ну и в заключение немного оффтопика. Я раньше любил программирование на атрибутах, но всё чаще сталкиваюсь с ограничениями подобного подхода. Например, майкрософт придумала fluent api в довесок к аннотациям, так как не всё можно просто описать атрибутами. Плюс я вижу преимущества в fluent validation (отсылаю к тому же видео). В общем-то всё. Не могу сказать, что это полноценный ответ на ваш вопрос — скорее больше пересказ моего опыта, поэтому если кому есть что дополнить из своей практики — буду рад почитать.

Комментариев нет:

Отправить комментарий