ASP.NET MVC 大型项目架构 [关闭]

Posted

技术标签:

【中文标题】ASP.NET MVC 大型项目架构 [关闭]【英文标题】:ASP.NET MVC Large Project Architecture [closed] 【发布时间】:2013-09-10 00:49:25 【问题描述】:

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

我以为我了解 MVC 的概念,但在研究了中型和大型应用程序的架构后,我感到困惑。 (尽量考虑可扩展性、可扩展性和持续维护)

当我试图考虑如何按照“最佳实践”指南(来自包括印刷和网络的众多来源)构建应用程序时,我感到困惑

尽量尊重

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

现在,当创建小型(基本、简单)MVC 应用程序时,所有这些几乎都在同一个项目中完成(在这种情况下我说的是 Visual Studio 项目),以及 MVC“层”之间的分离几乎只是 VS 项目中的文件夹(完全独立的命名空间)。

在我们的其他一些项目中,我们采用了服务 -> 存储库样式,所以这个不会有任何不同。

我们使用实体框架作为数据库持久性(数据库优先方法)。

我们已经将我们的数据库访问(EF 的东西)分离到另一个 VS 项目中,所以我们在解决方案中有一个 Web 项目和模型(或数据)项目。

网络项目有控制器和视图,数据项目有服务、存储库和 EF 的东西。

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

如果我要尝试遵循该方法(我认为),我将拥有一个域模型(EF 和存储库层处理的模型),然后我将拥有一个视图模型? (控制器和视图将处理的模型),现在这些不是 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

也许这是攻击它的“最佳”方法,但我以前没有遇到过这种方法。

我们将所有服务和存储库创建为接口,并使用结构映射作为 IoC 容器。我承认的另一件事是,即使我们正在使用依赖注入,我仍然在努力接受 TDD,感觉必须将所有内容都写两次(我认为是 DI 的全部要点)

我想我最终会向 SO 的那些愿意在构建大型 ASP.NET MVC 应用程序方面比我了解更多的人寻求帮助和指导。那里似乎有大量的信息,但一切似乎都非常概念化。当我最终谈到实现时,我迷失在概念中。

编辑

回应卡尔·安德森先生

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

【问题讨论】:

【参考方案1】:

您提出了一些有趣的问题和观点,关于构建 ASP.NET MVC 应用程序的多种方式,更不用说任何与此相关的应用程序了。我可以向您提供我对该主题的想法,您可以随意使用它。

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

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

【讨论】:

在我的帖子中添加了一个编辑作为回复 @OJay - 我完全理解您对我的回答的 cmets,并且像生活中的大多数事情一样,真正的答案介于两者之间(没有完美的架构),但我希望这可以帮助您感觉更好关于您最终做出或主张的决定。 :-) @OJay - 顺便说一句,如果您使用域模型或视图模型,批量分配漏洞仍然是一个问题,但我的观点是您倾向于拥有未显示的值域模型中的用户,而我认为视图模型应该只具有实际显示给用户的值,因此称为“视图模型”。无论你决定走哪条路,祝你好运。【参考方案2】:

当我最终谈到实现时,我迷失在概念中。

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

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

我相信这没问题。域模型描述了实际的对象(通常来自数据库)。视图模型描述了视图正确呈现所有内容所需的信息。两者通常不包含逻辑,仅包含属性列表。我认为这些几乎相同是很好的。使用Automapper 在模型之间轻松映射。

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

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

EF 使其所有模型类都是局部的。然后我们扩展同一个类并向它添加一个 MetaDataType 类来制作“视图模型”

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

TDD,感觉所有东西都要写两次

是的,可以。您采用的方法将抽象和实现分开了很多。它确实让人觉得还有更多要写的东西,但也更容易理解。尤其是因为您与接口而不是实现进行通信。

我正在呼吁 [...] 提供一些帮助和指导

虽然你提到了 TDD,但我不记得你提到过onion architecture。请务必通读 Jeffrey Palermo 的 The Onion Architecture。

观看 Jimmy Bogard 的 Put your controllers on a diet。

查看一些现有的示例解决方案,例如EFMVC 和CodeCampServer。

【讨论】:

我看过洋葱架构,但没有提及。一切都很好,我得到了一些概念,但又是一些我不确定如何实现的概念。 Automapper 看起来很有趣,而 EFMVC 看起来也很不错【参考方案3】:

我遇到了同样的困境。我不想创建与我的域模型几乎相同的视图模型,我不希望我的视图和控制器有时使用域模型而在其他时候查看模型,并且我并不总是想要公开每个属性的域模型。相反,我所做的是在域模型和视图模型之间创建一个中间层。该中间层将能够从任何视图模型返回域模型,并从域模型创建正确的视图模型。视图模型始终具有一个属性,即实际的域模型。例如,我的 AddressEditModel 有一个 Address 属性,它是我的 Address Domain Model。如果地址域模型具有我不想暴露的属性,那么当我使用中间层从地址编辑视图模型返回地址域模型时,它将从数据库中检索地址并使用视图模型的地址属性;否则(如果所有属性都可以公开),它只会返回我的视图模型的 Address 属性中的对象。这个解决方案允许我始终将视图模型层与我的视图和控制器一起使用,而无需两组几乎相同的类,同时仍然能够控制可以公开哪些属性。

【讨论】:

【参考方案4】:

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

背景

我通常遵循的基本概念是N-Tier archiecture 设计。它基本上被表述为“将领域/业务层与其他层(UI / Data Access)分开。主要目标是,当您的应用程序迁移到其他系统(UI / Storage)时,业务层保持不变。

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

通常N层的结构如下:

          entity

        interfaces

DataAccess  |  BusinessLogic

           UI

每一层都由组件(项目/解决方案)分隔,因此不强调每一层之间的耦合。

实体复制

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

public class OperationMessage
    public bool IsErrorget;set;
    public string OperationMessageget;set;

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

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

    在实体层修改基本的OperationMessage

    这是最容易做到的。但是,您打破了 n 层架构,因为现在您的业务层已经了解了“css”并且它指的是类似 Web 的架构。它添加了特定于 ui 的逻辑(在业务层中分配 CssClassName)。如果有一天你想将它迁移到C# Winform 或者Windows Mobile / Azure,它会污染架构。

    添加一个名为WebOperationMessage的新类

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

    继承OperationMessage

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

服务重复

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

但是说,如果您不进行单元测试或模拟,那么就这样做layering or duplication worth the effort?正如文章所引用的,

选择是否要构建分层应用程序。如果要分层,分离必须严格。如果不是,则不是分层应用程序。

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

值得吗?在某些情况下,是的。在其他情况下,例如企业应用程序,通常它更加僵化,并且不太可能发生迁移。但是,大多数时候企业可以扩展并且需要移动性。所以,zookeepers must become the rangers。

我的 2 美分

【讨论】:

以上是关于ASP.NET MVC 大型项目架构 [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET MVC,EntityFramework,DBContext,不同项目中的存储库[关闭]

在 ASP.NET MVC 4 中开发大型项目是不是合理?

ASP.NET MVC 项目架构

ASP.NET MVC 的项目建议 [关闭]

如何设置一个简单的ASP.NET MVC C#项目? [关闭]

ASP.Net MVC 3 中的多项目区域