ASP.NET MVC大型项目架构

OJa*_*Jay 15 c# architecture asp.net entity-framework asp.net-mvc-4

这是一个与如何为中型到大型应用程序构建ASP.NET MVC项目相关的问题.

我以为我理解了MVC的概念,但在研究了中型和大型应用程序的架构后,我很困惑.(试图考虑可扩展性,可扩展性和持续维护)

当我试图按照"最佳实践"的指导方针(来自包括印刷和网络的众多来源)来思考如何构建应用程序时,我的困惑就出现了

试图尊重像

  • 控制器应该保持非常简单
  • TDD原则(或者至少是将来使测试更容易的方法)
  • 分离关注
  • 服务和存储库
  • 依赖注入

现在在创建小型(基本的,简单的)MVC应用程序时,所有这些都在同一个项目中完成(在这种情况下我说的是Visual Studio Project),MVC"Layers"之间的分离非常多只是VS项目中的文件夹(完全独立的命名空间).

对于我们的一些其他项目,我们采用了服务 - >存储库样式,因此这一点不会有任何不同.

我们使用Entity Framework作为数据库持久性(DB第一种方法).

我们将数据库访问(EF内容)分离到另一个VS项目中,因此我们在解决方案中有一个Web项目和模型(或数据)项目.

Web项目具有控制器和视图,数据项目具有服务,存储库和EF内容.

我的困惑在于模型(或者可能理解域模型与视图模型)

如果我试图遵循这种方法(我认为),我会有一个域模型(EF和存储库层处理的模型),然后我会有一个视图模型?(Controller和视图将处理的模型),现在这些不是90%相同吗?这种方式是否只是让你两次编写模型代码?我确定我读过控制器和视图不应该有域模型的地方?

我们接近它的一种方法是EF使其所有模型类都是部分的.然后我们扩展同一个类并向其添加一个MetaDataType类来创建"视图模型"(将DataAnnotations添加到属性中)然后本质上相同的模型将通过所有层传递,但这是"最佳"实践(那里在我看来是一个分裂,这是不对的)

例如

[MetadataType(typeof(Product_Metadata))]
public partial class Product
{
    //Pretty much deliberately kept empty, just so
    // the EF model class can have the attribute added
    //The other side of this partial class is of course in the EF models
}

public class Product_Metadata
{
    [Required]
    [Display(Name = "Product name")]
    public string Name { get; set; }

    [Required]
    [Display(Name = "Unit Cost")]
    public decimal Cost { get; set; }

    //etc... for the rest of the properties on the product EF model
}
Run Code Online (Sandbox Code Playgroud)

也许这是攻击它的"最佳"方式,但我之前没有遇到过这种方法.

我们将所有服务和存储库创建为接口,并使用结构映射作为IoC容器.我承认的另一件事是,即使我们使用依赖注入,我仍然在努力与TDD达成协议,感觉就像必须写两次所有(我认为DI的整点)

我想最终我对SO的意愿很有吸引力,因为他们比我更了解构建大型ASP.NET MVC应用程序以获得一些帮助和指导.似乎有大量的信息,但似乎都是非常概念性的.当我最终实现时,我迷失在概念中.

编辑

回应卡尔安德森先生

  • 在视图中分页数据 - 是完全同意这是视图模型相关且有意义的地方,但同样是具有List属性的CategoryListViewModel,它是viewmodel类别或域模型类别的列表
  • 批量分配漏洞 - 我认为这个漏洞存在于域模型或视图模型中(毕竟如果真的需要设置IsAdmin,你肯定会在ViewModel上设置它).我认为这需要在不同的层处理,即授权,因此只使用窗帘角色只能设置IsAdmin
  • 以特定格式显示视图信息 - 当然这只是与模型绑定和/或查看html助手进行格式化 - 即仅查看视图和模型绑定问题.毕竟,通过视图呈现的所有模型的属性都以html结尾,并且此时都是字符串,因此无论如何都必须解析返回值,模型绑定的主要原则,所以如果我需要自定义的,只需写一个新的模型活页夹.
  • 使用您的域模型不仅仅是数据传输对象(DTO) - 我实际上尽可能地避免这种情况,试图坚持模型就是这样的事实,DTO.但是如果出现这种情况,我可能会在域模型上编写一个扩展方法,所有方法都没有被序列化,或者是添加一个视图模型,但它可能包含一个域模型
  • 具有相同域模型信息的不同抽象 - 部分同意.我会有一个PagedAccountListViewModel(仍然包含域模型)但我只会使用一个模型用于新的和更新帐户(我将新的处理与更新相同,只是预先填充的)并且它将是域模型

Row*_*man 8

当我最终实现时,我迷失在概念中.

这些概念非常重要,但也很抽象.很难想象如何最好地构建解决方案直到它完成之后(即为时已晚),没有人能真正告诉你如何构建它,因为每个项目都是如此不同.

我会有一个域模型[...]然后我会有一个视图模型?[...]这些不是90%相同吗?

我相信这没关系.域模型描述实际对象(通常来自数据库).视图模型描述了视图正确呈现所有内容所需的信息.两者通常不包含逻辑,只包含属性列表.我认为这几乎是完全相同的.使用Automapper可以轻松地在模型之间进行映射.

控制器和视图不应该具有域模型?

大多数开发人员更喜欢这种方法 我应该这样做.视图应该是一个视图模型,如果需要,控制器可以简单地在模型之间进行映射.

EF使其所有模型类都是部分的.然后我们扩展同一个类并为其添加一个MetaDataType类以生成"视图模型"

这是一个有趣的方法,但我不能推荐它.复制模型是可以接受的.

TDD,感觉必须要写两次

是的,它可以.您正在采用的方法将抽象和实现分开很多.它确实让人感觉有更多的东西要写,但它也更容易理解.特别是因为您使用接口而不是实现进行通信.

我呼吁提供一些帮助和指导


Kar*_*son 7

您提出了一些有趣的问题和观点,关于构建ASP.NET MVC应用程序的无数方法,更不用说任何应用程序了.我可以为您提供有关该主题的想法,您可以随意使用它.

你说你担心在创建域模型和视图模型时你会做两次相同的事情,因为它们看起来几乎相同.让我给你一些可能让你改变主意的场景:

  • 在视图中对数据进行分页 - 您可以让控制器请求a List<Category>,但您不希望您的域模型必须跟踪分页的元数据,例如总记录,页面大小和页码,对吗?A CategoryListViewModel会解决这个问题.
  • 批量分配漏洞 - 这与ASP.NET MVC如何帮助您的控制器代码通过模型绑定填充模型的数据有关.比如说,你有一个帐户页面,其中有UI上的名字,姓氏,电话号码等属性.您的帐户的域模型表示具有IsAdmin布尔值; 如果你使用域模型来填充视图,那么如果一个坏用户发现某个IsAdmin属性存在并传入IsAdmin=true了查询字符串,那么模型绑定器会选择它并使该帐户成为管理员,即使该视图从未显示过允许更改该值的字段.如果绑定到不包含该视图模型的视图模型IsAdmin 属性,而只是与视图相关的数据,然后这个漏洞就不存在了.
  • 以特定格式显示视图信息 - 假设您有一个显示电话号码的视图,而您的域模型将电话号码存储为数字类型(即int),然后直接将您的域模型绑定到您的视图将需要视图解析和格式化那个int价值.相反,视图模型可以将电话号码保存为字符串,已经正确格式化并只显示值.
  • 使用域模型不仅仅是数据传输对象(DTO) - 如果要将行为附加到域模型而不是仅仅保存数据,那么您将需要具有不包含用于显示数据的逻辑的视图模型.
  • 具有相同域模型信息的不同抽象 - 这与先前引用的分页场景相关联.你可能想有一个PagedAccountViewModel,AddNewAccountViewModel,UpdateAccountViewModel,等.


Fen*_*ndy 3

没有最佳实践/架构。每种设计都有缺点。关于您的目标架构和 90% 的代码重复,这是我的想法。它分为实体(DTO/模型)或服务/存储库。

背景

我通常遵循的基本概念是N层架构设计。它基本上被表述为“将域/业务层与其他层(UI /数据访问)分开。主要目标是,当您的应用程序迁移到其他系统(UI /存储)时,业务层保持不变。

如果您将 95% 的领域逻辑放入业务层(其他 5 个可能在数据库中,例如事务/报告生成),那么您几乎不需要更改任何内容,并且仍然具有相同的领域规则。领域层的问题解决了,你只需要专注于UI或存储(存储库)。

通常,N层的结构如下:

          entity

        interfaces

DataAccess  |  BusinessLogic

           UI
Run Code Online (Sandbox Code Playgroud)

每层都由程序集(项目/解决方案)分隔开,因此不强调每层之间的耦合。

实体复制

现在想象一个常见的“操作消息”类。我想象这个班级是这样的:

public class OperationMessage{
    public bool IsError{get;set;}
    public string OperationMessage{get;set;}
}
Run Code Online (Sandbox Code Playgroud)

随意修改类,添加枚举以进行警告等(这是使用自动属性的代码味道,如果您进行维护,请不要遵循)。

假设您的MVC应用程序具有名为“message_error”的 css,其中具有color:red;font-weight:bold;属性。通常您希望将其分配给具有诸如 之类的属性的类CssClassName。您有 3 个选择:

  1. OperationMessage修改实体层的基础

    这是最容易做的事情。然而,你打破了n层架构,因为现在你的业务层有了关于“css”的知识,它指的是类似web的架构。它添加特定于 ui 的逻辑(分配CssClassName业务层)。如果有一天你想将它迁移到C# Winform或者Windows Mobile/ Azure,它会污染架构。

  2. 添加一个名为的新类WebOperationMessage

    这就是我所说的ViewModel。现在它变得重复了,因为它与类有 90% 相似OperationMessage。但您的 N 层架构保持有序。OperationMessage从业务层获取对象后,您需要进行一些转换。这种转换,就是我所说的Presentation Logic

  3. 继承OperationMessage

    对于实体类型类来说,这可能是更好的方法。它确保您的 N 层架构保持有序,并且不会重复 90% 的代码。我还没有发现这个设计的缺陷,但也许有任何defensive-code风格的实体。但是,您仍然需要进行转换。

服务重复

这些服务已经在界面中重复。然而,正是由于实现了N-Tier架构,创建了持久性无知的代码。它使他们更容易进行单元测试和模拟。我希望读者已经了解模拟和单元测试,所以这个答案仍然相关。

但是,如果您不进行单元测试或模拟,那么这种分层或重复值得付出努力吗?正如文章中所引用的那样,

选择是您是否想要构建分层应用程序。如果想要层次感,分离一定要严格。如果不是,则它不是分层应用程序。

简而言之,一旦你违反/破坏了分层,你就失去了可移植性。如果您违反了 BLL / DAL 分层,您就失去了更改存储库的灵活性。如果您违反了 BLL / PL,您就失去了将应用程序从一种类型的 UI 迁移到另一种类型的灵活性。

这值得么?在某些情况下,是的。在其他情况下,例如企业应用程序,通常比较刚性,并且不太可能发生迁移。然而,很多时候企业可以扩张,需要流动性。所以,动物园管理员必须成为护林员

我的2分钱