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

Posted

技术标签:

【中文标题】将验证属性从域实体映射到 DTO【英文标题】:Mapping Validation Attributes From Domain Entity to DTO 【发布时间】:2011-01-05 17:26:17 【问题描述】:

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

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 等)并不重要。

在我的客户端层中,我也有一个标准设置,我不使用域实体本身,而是将它们映射到我的视图层使用的 ViewModel(又名 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 堆栈。 为什么需要 DTO?为什么不直接绑定到您的实体类? @Josh - 为了建立一个干净的分离级别,以及其他原因。无论如何,我认为讨论 DTO 模式是一个单独的话题。 看起来我不是唯一一个问这个问题的人。喜欢的人***.com/questions/2181940/… ***.com/questions/2547132/… 【参考方案1】:

如果您使用支持 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

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

【讨论】:

山姆,感谢您的意见。这似乎是一个很好的方法。我将进一步研究这种元数据方法是否适合域层。将在此处报告任何结果。 这里的问题是,如果您有一个 ViewModel 表示域对象中数据的 子集,您仍然必须在 ViewModel 中包含不需要的属性无论如何,否则您将收到运行时错误,因为某些属性无法映射。 @jonathonconway:如果您对域对象和 ViewModel 应用一组不同的验证规则(由于一组不同的属性),我会质疑尝试共享“一些”的价值其中。如果您的 ViewModel 与您的 Domain 对象不太相似,IMO 尝试对两者应用相同的验证不是一个有用的练习。也就是说,如果您正在编写自己的验证器来遵循这种方法,您可以让它忽略目标对象上不存在的属性。【参考方案2】:

验证的目的是确保进入您的应用程序的数据符合某些标准,请记住,验证属性约束(如您在此处确定的那些)有意义的唯一位置是您接受的点来自不受信任的来源(即用户)的数据。

您可以使用“金钱模式”之类的东西将验证提升到您的域类型系统中,并在有意义的视图模型中使用这些域类型。如果您有更复杂的验证(即,您要表达的业务规则需要比单个属性中表达的知识更多的知识),则它们属于应用更改的域模型上的方法。

简而言之,将数据验证属性放在您的视图模型中,并将它们从您的域模型中移除。

【讨论】:

如果我的域在多个客户端应用程序之间共享,每个客户端应用程序都有自己的域模型怎么办?然后我不需要在这两个客户端应用程序中复制验证逻辑吗? 假设您的意思是每个人都有自己的视图模型。只有属性应用于视图模型是重复的,验证逻辑/属性等可以通过公共验证库共享。您可以向您的域对象添加业务级别验证,以确保它们在持久化时有效,但恕我直言,这不仅仅是为了它而追逐重用。【参考方案3】:

为什么不使用界面来表达您的意图?例如:

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

    [NotLessThan0]
    decimal Price  get; set;

【讨论】:

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

事实证明,AutoMapper 或许能够自动为我们完成这项工作,这是最好的情况。

AutoMapper-users:将验证属性转移到视图模型?http://groups.google.com/group/automapper-users/browse_thread/thread/efa1d551e498311c/db4e7f6c93a77302?lnk=gst&q=validation#db4e7f6c93a77302

我还没有时间尝试那里提出的解决方案,但打算很快尝试。

【讨论】:

我看不到这是如何实现的,这个链接上的帖子不是很清楚。任何人都可以提供有关如何执行此操作的更多信息 google 小组讨论结束时说补丁已丢失。所以我相信它现在不在 Automapper 中? :(【参考方案5】:

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

【讨论】:

域实体已经在一个单独的程序集中:域。 ViewModel 模式的要点是您的域层不直接由您的客户端视图使用(关注点分离)。 @Martin:你需要关注点分离吗?我认为在可能的情况下,使用域对象而不是 ViewModel 很有用。如果不匹配,也许让 ViewModel 包装或装饰 Domain 对象,然后将 ViewModel 的验证委托给验证 Domain 对象的部分,并且任何特定于 View 的验证都可以在之后/之前完成。跨度> 【参考方案6】:

我也考虑了一段时间。我完全理解布拉德的回答。但是,假设我想使用另一个适用于注释域实体和视图模型的验证框架。

我能在纸上提出的唯一仍然适用于属性的解决方案是创建另一个“指向”您在视图模型中镜像的域实体属性的属性。这是一个例子:

// 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 这样的框架来处理这个新属性并在依赖类的属性上运行验证属性,但使用您的视图模型的属性值。我只是没有时间进一步充实这一点。

有什么想法吗?

【讨论】:

注意,这是不可能的,因为属性使用中缺少泛型和 lambda。【参考方案7】:

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

【讨论】:

@epitka - 我同意你所说的一切,这就是我目前遵循的模式。但是,我仍然发现我在重复验证:我的命令对象通常具有我需要在 DTO 上复制的属性级别验证。本质上,这和我原来的帖子是一样的问题,但是用 Command 代替 Entity,用 DTO 代替 ViewModel。我仍然需要复制验证元数据。我想是必要的邪恶吗?【参考方案8】:

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

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

我找不到从部分 ViewModel 中删除逻辑的可行方法,但我找到了一种传达相同 ErrorMessage 的方法,以便可以从单点维护它。由于 ErrorMessage 与视图相关联,因此它也可以是 ViewModel 的一部分。 consts被认为是静态成员,因此将错误消息定义为公共字符串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
        )
    )

警告:You might need to recompile all related projects that rely on const values...

【讨论】:

以上是关于将验证属性从域实体映射到 DTO的主要内容,如果未能解决你的问题,请参考以下文章

关于如何从域 (ORM) 对象映射到数据传输对象 (DTO) 的建议

应该使用哪个层从域对象转换为 DTO

从 DAL 返回 DTO 与 DataTable

我应该从域层抽象验证框架吗?

Grails Scaffolding Templates - 从域类中获取属性

如何使用 AutoMapper 将 json 请求 dto 中的 OData 枚举字符串映射到实体枚举属性