从领域实体到DTO的映射验证属性

34

我有一个标准的领域层实体:

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这样的自动化工具将我的领域实体映射到我的ViewModel DTO,是否也可以将映射属性的验证逻辑以某种方式转移到ViewModel中呢?

问题是:

1)这是一个好主意吗?

2)如果是的话,能否做到?如果不行,还有其他选择吗?

提前感谢您的任何帮助!


编辑:我应该提到我正在使用ASP.NET MVC。我最初认为这可能与此无关,但后来想到,在WinForms / WPF / Silverlight世界中可能有其他类型的解决方案(如MVVM),这些解决方案可能不适用于Web堆栈。 - Martin Suchanek
你为什么需要DTO呢?为什么不能直接绑定到实体类? - Josh Kodroff
3
为了建立清晰的分离层次,以及其他原因。无论如何,我认为辩论DTO模式是一个单独的话题。 - Martin Suchanek
看起来我不是唯一一个问这个问题的人。志同道合的人们:https://dev59.com/WHI95IYBdhLWcg3wtwf4 - anthonyv
https://dev59.com/anE85IYBdhLWcg3w64EA - Manish Jain
8个回答

12
如果您使用支持DataAnnotations的东西,您应该能够使用元数据类来包含您的验证属性:
public class ProductMetadata 
{
    [NotEmpty, NotShorterThan10Characters, NotLongerThan100Characters]
    public string Name { get; set; }

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

并将其添加到领域实体和 DTO 的 MetadataTypeAttribute 中:

[MetadataType(typeof(ProductMetadata))]
public class Product
并且
[MetadataType(typeof(ProductMetadata))]
public class ProductViewModel

这种方法可能不能适用于所有验证器 - 你可能需要扩展你选择的验证框架来实现类似的方法。


Sam,感谢您的建议。这似乎是一个不错的方法。我将进一步研究这种元数据方法是否适用于域层。有任何结果我会在这里报告。 - Martin Suchanek
4
这里的问题在于,如果您有一个ViewModel来表达域对象中的子集数据,那么您仍然必须在ViewModel中包含不需要的属性,否则您将会因为某些属性无法映射而得到运行时错误。 - Jonathan
1
@jonathonconway: 如果您对“域(Domain)”对象和视图模型(ViewModel)应用不同的验证规则(由于不同的属性集),我会质疑尝试共享“某些”验证规则的价值。如果您的视图模型与域对象非常不相似,我认为尝试在两者之间应用相同的验证无益处。话虽如此,如果您正在编写遵循此方法的自己的验证器,则可以使其忽略目标对象上不存在的属性。 - Sam

9
验证的目的是确保进入应用程序的数据满足某些标准。因此,只有在接受来自不可信源(即用户)的数据时才有意义验证属性约束,就像您在此处识别的那样。
您可以使用“货币模式”之类的东西将验证提升到您的域类型系统中,并在视图模型中使用这些域类型。如果您有更复杂的验证(即表达需要比单个属性中表达的知识更多的业务规则),则应将其放在应用更改的域模型上的方法中。
简而言之,在视图模型上放置数据验证属性,并将其从域模型中删除。

3
如果我的域名在多个客户端应用程序之间共享,每个应用程序都有自己的领域模型,那我是不是需要在这些客户端应用程序中复制验证逻辑? - Martin Suchanek
3
假设你的意思是每个都有自己的视图模型。只有将属性应用于视图模型是重复的,验证逻辑/属性等可以通过共用的验证库进行共享。你可以向域对象添加业务级别的验证以确保它们在持久化时是有效的,但在我看来,除此之外再追求重用就是无谓的了。 - Neal

4
为什么不使用接口来表达您的意图?例如:
public interface IProductValidationAttributes {
    [NotEmpty, NotShorterThan10Characters, NotLongerThan100Characters]
    string Name { get; set; }

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

嗯,有趣的方法。我假设IProductValidationAttributes接口将在Domain层中定义?并由Product领域实体和ProductViewModel实现?如果是这样,那么这不会破坏ViewModel的目的吗?如果它必须在Domain层中实现一个接口?如果我的View必须依赖于Domain层,那么我还不如直接使用原始的领域实体本身,对吧? - Martin Suchanek
-1,因为接口规定ViewModel和Domain Model中的数据类型必须相同,但通常情况下并非如此。ViewModel通常由字符串组成,而域对象包含整数、小数、布尔值等。 - Valentin V
@Martin。如果我要做这个,我会把接口放在一个单独的项目中。但我不会采用这种方法。我不喜欢在领域中使用属性进行验证(您不应该允许您的领域对象进入无效状态)。我认为您尝试重用此逻辑将会给自己带来更多的痛苦,何不直接在领域端进行验证(在不变式的构造函数/工厂中进行验证,在命令中进行其他所有操作)?如果您只是进行CRUD操作,我可能会使用活动记录模式而不是DDD,并直接使用对象。 - JontyMC

4

2
我不知道这是如何实现的,这个链接中的帖子不是很清晰。有人能提供更多关于如何做到这一点的信息吗? - ricardo
1
谷歌小组讨论结束时说补丁已经丢失了。所以我认为现在Automapper中没有它了?:( - Narayana

1

我也已经考虑了一段时间。我完全理解 Brad 的回复。然而,假设我想使用另一个适用于注释领域实体和视图模型的验证框架。

在纸上,我能想到的唯一仍然与属性一起工作的解决方案是创建另一个属性,该属性“指向”领域实体中您在视图模型中镜像的属性。以下是一个示例:

// 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这样的框架可能会被扩展来处理这个新属性,并在依赖类的属性上运行验证属性,但使用您的视图模型的属性值。我只是没有时间进一步完善它。
有什么想法吗?

5
抱歉,由于属性使用中缺乏泛型和lambda表达式,这是不可能的。 - ventaur

1
如果您使用手写的领域实体,为什么不将它们放在自己的程序集中,并在客户端和服务器上同时使用同一个程序集。这样可以重用相同的验证。

1
领域实体已放在一个单独的程序集中:Domain。ViewModel模式的重点在于您的客户端视图不直接消费领域层(关注点分离)。 - Martin Suchanek
@Martin:你需要关注点分离吗?我认为,在可能的情况下,使用领域对象而不是ViewModel很有用。如果存在不匹配,也许可以让ViewModel包装或修饰领域对象,然后将ViewModel的验证委托给验证领域对象的部分,并且任何特定于View的验证可以在之后/之前进行。 - jamiebarrow

0

免责声明:我知道这是一个旧的讨论,但它最接近我所寻找的内容:通过重用验证属性保持DRY。我希望它不会离原始问题太远。

在我的情况下,我想要在.NET视图和其他视图模型中使错误消息可用。我们的实体几乎没有业务逻辑,主要针对数据存储。相反,我们有一个具有验证和业务逻辑的大型视图模型,我想重用错误消息。由于用户只关心错误消息,因此我认为这是相关的,因为这是易于维护的重要内容。

我找不到从部分视图模型中删除逻辑的可行方法,但我找到了一种传达相同ErrorMessage的方法,以便可以从单个点进行维护。由于ErrorMessages与视图绑定,因此它也可以成为ViewModel的一部分。 Const被认为是静态成员,因此将错误消息定义为public string constants,我们可以在类外部访问它们。

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来创建下拉列表,因此无法在Razor中使用@Html.EditorFor助手,因此也无法使用非侵入式验证。有了错误消息可用,我们现在可以应用必要的属性:
    @(Html.Kendo().DropDownList()
        .Name("TopicIds")
        .HtmlAttributes(new {
            @class = "form-control",
            data_val = "true",
            data_val_required = SupervisionViewModel.TopicIdsErrorMessage
        })
    )

警告:您可能需要重新编译所有依赖于常量值的相关项目...


0
首先,"标准"领域实体的概念是不存在的。对我来说,标准领域实体一开始就没有任何setter。如果你采用这种方法,你可以拥有更有意义的API,它实际上传达了关于你的领域的一些信息。因此,你可以有一个应用程序服务来处理你的DTO,创建命令,直接对你的领域对象执行,例如SetContactInfo、ChangePrice等。每个命令都可以引发ValidationException,然后你可以在你的服务中收集它们并呈现给用户。你仍然可以将属性留在dto的属性上进行简单的属性/属性级别验证。对于其他任何事情,请咨询你的领域。即使这是一个CRUD应用程序,我也会避免将我的领域实体暴露给表示层。

@epitka - 我同意你所说的一切,这也是我目前遵循的模式。然而,我仍然发现我在重复验证:我的命令对象通常具有属性级别的验证,我需要在DTO上进行复制。本质上,这与我的原始帖子相同,但将实体替换为命令,将ViewModel替换为DTO。我仍然需要复制验证元数据。我想这是必要的恶吧? - Martin Suchanek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接