MVC 3 应用程序中的模型、视图模型、DTO

Posted

技术标签:

【中文标题】MVC 3 应用程序中的模型、视图模型、DTO【英文标题】:Models, ViewModels, DTOs in MVC 3 application 【发布时间】:2011-08-25 02:33:42 【问题描述】:

我有一个包含两个子项目的 Web 解决方案(在 VS2010 中):

    Domain 包含 Model 类(通过实体框架映射到数据库表)和 Services(除了其他东西)负责 CRUD 操作

    WebUI 引用域项目

对于我创建的第一页,我在强类型视图中直接使用域项目中的模型类作为模型,因为这些类很小,我想显示和修改 所有 属性.

现在我有一个页面,它应该只适用于相应领域模型的所有属性的一小部分。我通过在我的服务类中使用查询结果的 投影 来检索这些属性。但是我需要投射到一个类型中 - 我的问题是关于我能想到的解决方案:

    我介绍了ViewModels,它存在于WebUI 项目中,并将IQueryablesEF data context 从服务公开到WebUI 项目。然后我可以直接投影到那些 ViewModel 中。

    如果我不想公开 IQueryables 和 EF 数据上下文,我将 ViewModel 类放在 Domain 项目中,然后我可以直接返回 ViewModel 作为查询和投影的结果服务类。

    除了WebUI 项目中的ViewModels,我还介绍了Data transfer objects,它将数据从服务类中的查询移动到ViewModels

解决方案 1 和 2 看起来工作量相同,我更喜欢解决方案 2 以将所有数据库问题保留在单独的项目中。但不知何故,在 Domain 项目中拥有 View-Models 听起来是错误的。

解决方案 3 听起来需要做更多的工作,因为我要创建更多类并关心 Model-DTO-ViewModel 映射。我也不明白 DTO 和 ViewModel 之间有什么区别。 ViewModels 不正是我想要显示的模型类的选定属性的集合吗?它们不会包含与 DTO 相同的成员吗?为什么要区分 ViewModel 和 DTO?

这三种解决方案中哪一种更可取,有哪些优点和缺点?还有其他选择吗?

提前感谢您的反馈!

编辑(因为我的文字墙可能太长并且被要求提供代码)

示例:我有一个Customer 实体...

public class Customer

    public int ID  get; set; 
    public string Name  get; set; 
    public City  get; set; 
    // ... and many more properties

...并且想要创建一个仅显示(并且可能允许编辑)列表中客户的Name 的视图。在服务类中,我通过投影提取视图所需的数据:

public class CustomerService

    public List<SomeClass1> GetCustomerNameList()
    
        using (var dbContext = new MyDbContext())
        
            return dbContext.Customers
                .Select(c => new SomeClass1
                             
                                 ID = c.ID,
                                 Name = c.Name
                             )
                .ToList();
        
    

然后有一个带有操作方法的 CustomerController。这应该是什么样子?

无论哪种方式(a)...

public ActionResult Index()

    List<SomeClass1> list = _service.GetCustomerNameList();
    return View(list);

...或者更好的方式(b):

public ActionResult Index()

    List<SomeClass1> list = _service.GetCustomerNameList();

    List<SomeClass2> newList = CreateNewList(list);

    return View(newList);

关于上面的选项 3,我想说:SomeClass1(位于 Domain 项目中)是一个 DTOSomeClass2(位于 WebUI 项目中)是一个 视图模型。

我想知道区分这两个类是否有意义。为什么我不总是为控制器操作选择选项(a)(因为它更容易)?除了 DTO (SomeClass1) 之外,是否还有理由引入 ViewModel (SomeClass2)?

【问题讨论】:

@jfar:现在有一个带有代码的示例。 我很好奇 - 您选择的方法对您有什么效果?? @qntmfred:我很满意。我现在使用 GET 请求的已接受答案中的方法(将 EF 模型直接投影到 ViewModel 的扩展方法,投影发生在 DB 中,不涉及 DTO)。对于 POST 请求,我将进入 POST 操作的 ViewModel 映射到 DTO,然后使用该 DTO 调用服务方法。 (我不直接映射到 EF 模型,因为有时会涉及到业务逻辑,我不想在表示层中有它,并且我不能将 ViewModel 用于服务,因为服务层不知道ViewModel 类。) 太棒了。感谢您的更新。 很好的问题,很好的讨论。好像您读懂了我的想法并发布了问题。谢谢! 【参考方案1】:

我会通过使用自动映射工具(如AutoMapper)为您进行映射来解决您的问题。在映射很容易的情况下(例如,如果一个类的所有属性都应该映射到另一个类上具有相同名称的属性),AutoMapper 将能够为您完成所有连接工作,您必须给出几行代码来说明两者之间应该有一个映射。

这样,您可以将实体放在Domain 中,将几个视图模型类放在WebUI 中,并在某个地方(最好在WebUI 或相同的子命名空间中)定义它们之间的映射。您的视图模型实际上将成为 DTO,但您不必担心域和 DTO 类之间的转换过程。

注意:我强烈建议不要将您的域实体直接提供给您的 MVC Web UI 的视图。您不希望 EF 一直“停留”到前端层,以防您以后想使用 EF 以外的东西。

【讨论】:

“您的视图模型实际上是 DTO”:您的意思是我不需要单独的 DTO 类吗?记住“投影”:我不查询完整的实体,而只查询属性的子集。我会将这些属性投影到哪种类型?如果我没有单独的 DTO,我需要将它们直接投影到 ViewModel 中,不是吗?但是我的 ViewModel 类将成为 Domain 项目的一部分,而不是 WebUI。 +1 - 我问了similar question,得到了一些很好的回应。这些也可能有帮助。 @Slauma:由于您使用的是 EF,默认情况下使用延迟加载,您可以在控制器中进行投影,并将投影直接映射到您 ViewModels,而无需获取更多数据比你从数据库中需要的多。【参考方案2】:

介绍生活在 WebUI 项目并公开 IQueryables 和来自的 EF 数据上下文 为 WebUI 项目提供服务。然后我 可以直接投射到那些 视图模型。

这样做的问题是您很快就会在使用 EF 尝试“扁平化”模型时遇到问题。当我有一个看起来像这样的CommentViewModel 类时,我遇到了类似的情况:

public class CommentViewModel

    public string Content  get; set; 
    public string DateCreated  get; set; 

CommentViewModel 的以下 EF4 查询投影不能作为 couldn't translate the ToString() method into SQL 工作:

var comments = from c in DbSet where c.PostId == postId 
               select new CommentViewModel() 
                
                   Content = c.Content,
                   DateCreated = c.DateCreated.ToShortTimeString() 
               ;

使用 Automapper 之类的工具是一个不错的选择,尤其是当您需要进行大量转换时。但是,您也可以创建自己的转换器,基本上将您的域模型转换为您的视图模型。就我而言,我创建了自己的扩展方法来将我的Comment 域模型转换为我的CommentViewModel,如下所示:

public static class ViewModelConverters

    public static CommentViewModel ToCommentViewModel(this Comment comment)
    
        return new CommentViewModel() 
         
            Content = comment.Content,
            DateCreated = comment.DateCreated.ToShortDateString() 
        ;
    

    public static IEnumerable<CommentViewModel> ToCommentViewModelList(this IEnumerable<Comment> comments)
    
        List<CommentViewModel> commentModels = new List<CommentViewModel>(comments.Count());

        foreach (var c in comments)
        
            commentModels.Add(c.ToCommentViewModel());
        

        return commentModels;
    

基本上,我所做的是执行标准 EF 查询以恢复域模型,然后使用扩展方法将结果转换为视图模型。例如以下方法说明用法:

public Comment GetComment(int commentId)

    return CommentRepository.GetById(commentId);


public CommentViewModel GetCommentViewModel(int commentId)

    return CommentRepository.GetById(commentId).ToCommentViewModel();


public IEnumerable<Comment> GetCommentsForPost(int postId)

    return CommentRepository.GetCommentsForPost(postId);


public IEnumerable<CommentViewModel> GetCommentViewModelsForPost(int postId)

    return CommentRepository.GetCommentsForPost(postId).ToCommentViewModelList();

【讨论】:

嗯,但是这些扩展方法不能解决您将 DateTime 直接投影到 EF 查询中的字符串的问题,对吗?扩展方法仅适用于“完整”评论对象。但是如果你想要一个投影,你不会使用Comment,而是使用一些中间类型。那你在做什么?您不能使用您的 ViewModel (因为您描述的问题)并且您不想在查询中加载完整的 Comment 对象。那么,您将投影到哪种类型?这不会导致中间的第三种类型 - 最后是DTO @Slauma 我已经更新了我的答案以展示我如何使用它们。基本上我从 EF 返回一个域模型,然后将其转换为等效的视图模型。 @Dan Diplo:我明白了,那么您的查询中基本上没有投影了。您加载完整的对象,然后在内存中“项目”到您的 ViewModel。如果域对象很小,这在许多情况下可能没问题。但是考虑一下您有一个大型域对象的情况,比如说 50 个属性,但您只想在网页上显示 10 个属性。然后,您将有一个“查询开销”并加载 40 个您不需要的属性。 @Slauma 在编程中始终需要在易于开发/可维护性和性能之间取得平衡。在大多数情况下,我的方法的开销很小,但是对于高性能站点,是的,它会增加性能损失。但是所有的抽象都有这个问题。如果您不介意所有额外的输入,我想您可以然后投影到中间类型并从中转换!但是,如果性能真的很重要,那么 EF 可能不是要选择的 ORM。请参阅 Dapper - code.google.com/p/dapper-dot-net @Dan Diplo:我完全同意。我可能会混合:对于小实体,我查询完整对象并创建一个 ViewModel。对于所描述的视图(仅显示 50 个属性中的 10 个),我将介绍一个中间类型。再次感谢您的全面回答!【参考方案3】:

谈论模型、视图模型和 DTO 令人困惑,我个人不喜欢使用这些术语。我更喜欢谈论 Domain EntitiesDomain ServicesOperation Input/Result(又名 DTO)。所有这些类型都存在于域层中。操作是实体和服务的行为。除非您正在构建一个纯 CRUD 应用程序,否则表示层只处理输入/结果类型,而不是实体。您不需要额外的 ViewModel 类型,这些是 ViewModel(换句话说,视图的模型)。视图用于将操作结果转换为 html,但相同的结果可以序列化为 XML 或 JSON。您用作 ViewModel 的是领域的一部分,而不是表示层。

【讨论】:

好的,这基本上是我的选择(2)(用你的“操作输入/结果”替换我的术语“ViewModel”)。 这里有点晚了 :) 我认为有些人会不同意。 ViewModel 确实可以包含表示逻辑,因此它似乎使其成为表示层的一部分。 “ViewModel 封装了表示逻辑和状态”blogs.msdn.com/b/dphill/archive/2009/01/31/… @AdamTuliper 是的,您可以在表示层中使用 ViewModel,我只是说这不是必需的,您可以使用从域层获得的 DTO 作为 ViewModel。

以上是关于MVC 3 应用程序中的模型、视图模型、DTO的主要内容,如果未能解决你的问题,请参考以下文章

在 ASP.NET MVC 5 中搭建外部模型

何时在 MVC4 应用程序中使用 DTO(数据传输对象)? [复制]

领域实体、DTO 和视图模型

如何在没有模型的情况下验证 MVC3 中的强类型视图

浅谈js中的MVC

简单谈谈js中的MVC