Сопоставление атрибутов проверки от объекта домена до DTO

У меня есть стандартный объект уровня домена:

public class Product
{
    public int Id { get; set; }

    public string Name { get; set; }

    public decimal Price { get; set;}
}

к которому применены какие-то атрибуты проверки:

public class Product
{
    public int Id { get; set; }

    [NotEmpty, NotShorterThan10Characters, NotLongerThan100Characters]
    public string Name { get; set; }

    [NotLessThan0]
    public decimal Price { get; set;}
}

Как видите, я полностью придумал эти атрибуты. Используемая среда проверки (NHibernate Validator, DataAnnotations, ValidationApplicationBlock, Castle Validator и т. Д.) Не имеет значения.

На моем клиентском уровне у меня также есть стандартная настройка, в которой я не использую сами сущности домена, а вместо этого сопоставляю их с моделями представления (также известными как DTO), которые использует мой слой представления:

public class ProductViewModel
{
    public int Id { get; set; }

    public string Name { get; set; }

    public decimal Price { get; set;}
}

Допустим, я хочу, чтобы мой клиент / представление мог выполнять некоторые базовые проверки на уровне свойств.

Единственный способ, которым я могу это сделать, - это повторить определения валидации в объекте ViewModel:

public class ProductViewModel
{
    public int Id { get; set; }

    // validation attributes copied from Domain entity
    [NotEmpty, NotShorterThan10Characters, NotLongerThan100Characters]
    public string Name { get; set; }

    // validation attributes copied from Domain entity
    [NotLessThan0]
    public decimal Price { get; set;}
}

Это явно неудовлетворительно, поскольку я повторил бизнес-логику (проверка на уровне свойств) на уровне ViewModel (DTO).

Так что же можно сделать?

Предполагая, что я использую инструмент автоматизации, такой как AutoMapper, для сопоставления сущностей моего домена с моими DTO ViewModel, было бы также неплохо каким-то образом перенести логику проверки для сопоставленных свойств в ViewModel?

Вопросы следующие:

1) Это хорошая идея?

2) Если да, то можно ли это сделать? Если нет, то каковы альтернативы, если таковые имеются?

Заранее благодарим вас за любой вклад!


person Martin Suchanek    schedule 15.01.2010    source источник
comment
РЕДАКТИРОВАТЬ: Полагаю, я должен упомянуть, что работаю с ASP.NET MVC. Первоначально я думал, что это может быть неактуально, но потом решил, что в мире WinForms / WPF / Silverlight есть другие типы решений (например, MVVM), которые могут не применяться к веб-стеку.   -  person Martin Suchanek    schedule 16.01.2010
comment
Зачем вам вообще нужен DTO? Почему бы просто не привязаться к своему классу сущности?   -  person Josh Kodroff    schedule 26.01.2010
comment
@Josh - Среди прочего, чтобы установить чистый уровень разделения. В любом случае, я думаю, что обсуждение паттерна DTO - это отдельная тема.   -  person Martin Suchanek    schedule 27.01.2010
comment
похоже, что я не единственный, кто задает этот вопрос. Как умы stackoverflow.com/questions/2181940/   -  person anthonyv    schedule 04.02.2010
comment
stackoverflow.com/questions/2547132/   -  person Manish Jain    schedule 07.06.2015


Ответы (8)


Если вы используете что-то, поддерживающее DataAnnotations, у вас должна быть возможность использовать класс метаданных для хранения ваших атрибутов проверки:

public class ProductMetadata 
{
    [NotEmpty, NotShorterThan10Characters, NotLongerThan100Characters]
    public string Name { get; set; }

    [NotLessThan0]
    public decimal Price { get; set;}
}

и добавьте его в атрибут MetadataTypeAttribute как для объекта домена, так и для DTO:

[MetadataType(typeof(ProductMetadata))]
public class Product

и

[MetadataType(typeof(ProductMetadata))]
public class ProductViewModel

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

person Sam    schedule 11.02.2010
comment
Сэм, спасибо за вклад. Это кажется хорошим подходом. Я собираюсь провести еще несколько исследований относительно того, подходит ли этот подход к метаданным для уровня домена. О любых результатах сообщу здесь. - person Martin Suchanek; 18.02.2010
comment
Проблема здесь в том, что если у вас есть ViewModel, который выражает подмножество данных в объекте Domain, вам все равно придется включать ненужные свойства в вашу ViewModel, иначе вы получите ошибка времени выполнения из-за невозможности сопоставления некоторых свойств. - person Jonathan; 22.04.2010
comment
@jonathonconway: если вы применяете другой набор правил проверки к объекту Domain и ViewModel (из-за другого набора атрибутов), я бы поставил под сомнение ценность попытки поделиться «некоторыми» из них. Если ваша ViewModel не очень похожа на ваш объект Domain, IMO это бесполезное упражнение, чтобы пытаться применить одинаковую проверку к обоим. Тем не менее, если вы пишете свой собственный валидатор, который следует этому подходу, вы можете заставить его игнорировать свойства, которые не существуют в целевом объекте. - person Sam; 23.04.2010

Цель проверки - убедиться, что данные, поступающие в ваше приложение, соответствуют определенным критериям, имея в виду, что единственное место, где имеет смысл проверять ограничения свойств, подобные тем, которые вы здесь определили, - это момент, когда вы принимаете данные из ненадежный источник (т.е. пользователь).

Вы можете использовать что-то вроде «денежного шаблона», чтобы повысить уровень валидации в вашей системе типов домена и использовать эти типы домена в модели представления, где это имеет смысл. Если у вас более сложная проверка (т. Е. Вы выражаете бизнес-правила, требующие более глубоких знаний, чем те, которые выражены в отдельном свойстве), они относятся к методам модели предметной области, которые применяют изменения.

Короче говоря, поместите атрибуты проверки данных в свои модели представления и оставьте их вне моделей предметной области.

person Neal    schedule 10.02.2010
comment
Что, если мой домен используется несколькими клиентскими приложениями, каждое со своими собственными моделями домена? Разве тогда мне не нужно было бы дублировать логику проверки в обоих этих клиентских приложениях? - person Martin Suchanek; 11.02.2010
comment
Предполагая, что вы имеете в виду каждый со своими собственными моделями представления. Дублируется только применение атрибутов для просмотра моделей, логика / атрибуты проверки и т. Д. Могут совместно использоваться через общую библиотеку проверки. Вы можете добавить проверку бизнес-уровня к своим объектам домена, чтобы убедиться, что они действительны при сохранении, но IMHO больше, чем это, преследует повторное использование ради него. - person Neal; 15.02.2010

Почему бы не использовать интерфейс, чтобы выразить свои намерения? Например:

public interface IProductValidationAttributes {
    [NotEmpty, NotShorterThan10Characters, NotLongerThan100Characters]
    string Name { get; set; }

    [NotLessThan0]
    decimal Price { get; set;}
}
person JontyMC    schedule 16.01.2010
comment
Хм. Интересный подход. Я предполагаю, что интерфейс IProductValidationAttributes будет определен на уровне домена? И реализуется как сущностью предметной области, так и ProductViewModel? Если так, разве это не уничтожит цель модели просмотра? Если он должен реализовать интерфейс на уровне домена? Если мое представление должно зависеть от уровня домена, тогда я мог бы использовать сами исходные сущности домена, не так ли? - person Martin Suchanek; 17.01.2010
comment
-1, потому что интерфейс диктует одни и те же типы данных как в ViewModel, так и в модели предметной области, но обычно это не так. ViewModels обычно состоят из строк, а объекты домена содержат целые числа, десятичные числа, логические значения и т. Д. - person Valentin V; 17.01.2010
comment
@Мартин. Если бы я собирался это сделать, у меня был бы интерфейс в отдельном проекте. Но я бы не пошел на такой подход. Мне не нравится использовать атрибуты для проверки, особенно в домене (вы не должны позволять объектам вашего домена переходить в недопустимое состояние). Я думаю, вы причините себе больше боли, чем стоит пытаться повторно использовать эту логику, почему бы просто не проверить на стороне домена (в конструкторе / фабрике для инварианта и в команде для всего остального)? Если вы просто выполняете CRUD, я бы, вероятно, использовал шаблон активной записи, а не ddd, и использовал бы объекты напрямую. - person JontyMC; 18.01.2010

Оказывается, AutoMapper может сделать это за нас автоматически, что является лучшим сценарием.

Пользователи AutoMapper: переносить атрибуты проверки в модель просмотра?
http://groups.google.com/group/automapper-users/browse_thread/thread/efa1d551e498311c/db4e7f6c93a77302?lnk=gst&q=validation#cb93a7

У меня пока нет времени опробовать предлагаемые решения, но я собираюсь это сделать в ближайшее время.

person Martin Suchanek    schedule 20.02.2010
comment
Я не вижу, как это было реализовано, сообщение по этой ссылке не очень понятно. Может ли кто-нибудь предоставить дополнительную информацию о том, как это сделать - person ricardo; 30.06.2011
comment
Обсуждение в группе Google заканчивается тем, что патч утерян. Полагаю, теперь его нет в Automapper? :( - person Narayana; 28.01.2014

Если вы используете рукописные сущности домена, почему бы не поместить сущности домена в их собственную сборку и не использовать ту же сборку как на клиенте, так и на сервере. Вы можете повторно использовать одни и те же проверки.

person ASharp    schedule 16.02.2010
comment
Сущности домена уже находятся в отдельной сборке: Domain. Суть паттерна ViewModel заключается в том, что уровень вашего домена не используется непосредственно вашими клиентскими представлениями (разделение проблем). - person Martin Suchanek; 18.02.2010
comment
@Martin: вам нужно разделение проблем? Я думаю, что там, где это возможно, полезно использовать объекты Domain вместо ViewModel. Если есть несоответствие, возможно, ViewModel обернет или украсит объект Domain, а затем делегирует проверку ViewModel той части, которая проверяет объект Domain, и любая проверка, специфичная для View, может быть выполнена после / до. - person jamiebarrow; 02.11.2010

Я уже некоторое время обдумываю это. Я полностью понимаю ответ Брэда. Однако предположим, что я хочу использовать другую структуру проверки, которая подходит для аннотирования как сущностей предметной области, так и моделей представления.

Единственное решение, которое я могу придумать на бумаге, которое все еще работает с атрибутами, - это создать другой атрибут, который «указывает» на свойство сущности предметной области, которое вы зеркалируете в своей модели представления. Вот пример:

// In UI as a view model.
public class UserRegistration {
  [ValidationDependency<Person>(x => x.FirstName)]
  public string FirstName { get; set; }

  [ValidationDependency<Person>(x => x.LastName)]
  public string LastName { get; set; }

  [ValidationDependency<Membership>(x => x.Username)]
  public string Username { get; set; }

  [ValidationDependency<Membership>(x => x.Password)]
  public string Password { get; set; }
}

Фреймворк, такой как xVal, возможно, может быть расширен для обработки этого нового атрибута и запуска атрибутов проверки для свойства класса зависимости, но со значением свойства вашей модели представления. У меня просто не было времени подробно описывать это.

Есть предположения?

person ventaur    schedule 17.02.2010
comment
Обратите внимание: это невозможно из-за отсутствия как универсальных, так и лямбда-выражений в использовании атрибутов. - person ventaur; 16.09.2012

Во-первых, нет понятия «стандартная» предметная область. Для меня стандартный объект домена не имеет никаких сеттеров. Если вы воспользуетесь этим подходом, у вас может быть более значимый api, который действительно что-то сообщает о вашем домене. Таким образом, у вас может быть служба приложения, которая обрабатывает ваш DTO, создает команды, которые вы можете выполнять непосредственно с объектами вашего домена, такими как SetContactInfo, ChangePrice и т. Д. Каждый из них может вызывать исключение ValidationException, которое, в свою очередь, вы можете собирать в своей службе и представлять для Пользователь. Вы по-прежнему можете оставить свои атрибуты в свойствах dto для простой проверки уровня атрибута / свойства. По поводу всего остального обратитесь к своему домену. И даже если это приложение CRUD, я бы избегал подвергать объекты своего домена уровню представления.

person epitka    schedule 25.01.2010
comment
@epitka - Я согласен со всем, что вы сказали, и сейчас я следую этому шаблону. Однако я все еще обнаруживаю, что дублирую валидацию: у моих объектов команд обычно есть валидации на уровне свойств, которые мне нужно дублировать в DTO. По сути, это та же проблема, что и в моем исходном сообщении, но замените Command на Entity и DTO на ViewModel. Мне все еще нужно продублировать метаданные проверки. Полагаю, необходимое зло? - person Martin Suchanek; 27.01.2010

Заявление об ограничении ответственности: я знаю, что это старое обсуждение, но оно было ближе всего к тому, что я искал: сохранение СУХОЙ за счет повторного использования атрибутов проверки. Я надеюсь, что это не так уж и далеко от первоначального вопроса.

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

Мне не удалось найти реальный способ удалить логику из частичных ViewModels, но я нашел способ передать одно и то же сообщение ErrorMessage, чтобы его можно было поддерживать из одной точки. Поскольку сообщения ErrorMessages привязаны к представлению, оно также может быть частью ViewModel. Константы считаются статическими членами, поэтому, определяя сообщения об ошибках как муравьи с общедоступной строкой const, мы можем получить к ним доступ вне класса.

public class LargeViewModel
{
    public const string TopicIdsErrorMessage = "My error message";

    [Required(ErrorMessage = TopicIdsErrorMessage)]
    [MinimumCount(1, ErrorMessage = TopicIdsErrorMessage)]
    [WithValidIndex(ErrorMessage = TopicIdsErrorMessage)]
    public List<int> TopicIds { get; set; }
}

public class PartialViewModel
{
    [Required(ErrorMessage = LargeViewModel.TopicIdsErrorMessage]
    public List<int> TopicIds { get; set; }
}

В нашем проекте мы использовали собственный html для выпадающих списков, так что мы не могли использовать помощник @ Html.EditorFor в razor, поэтому мы не могли использовать ненавязчивую проверку. Имея сообщение об ошибке, мы можем применить необходимые атрибуты:

    @(Html.Kendo().DropDownList()
        .Name("TopicIds")
        .HtmlAttributes(new {
            @class = "form-control",
            data_val = "true",
            data_val_required = SupervisionViewModel.TopicIdsErrorMessage
        })
    )

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

person pekaaw    schedule 12.06.2019