16

This is a question related to how to structure an ASP.NET MVC project for a medium to large application.

I thought I understood the concepts of MVC but after looking into architectures for medium and large applications I am confused. (trying to take into consideration scalability, extensibility and ongoing maintenance)

My confusion come in when I try to think of how to structure an application following guidelines of 'best practices' (from many and numerous sources included printed and web)

Trying to respect things like

  • Controllers should be kept very simple
  • TDD principles (or at least an approach that will make testing easier in the future)
  • Separation of concern
  • Services and repositories
  • Dependency injection

Now when creating small (basic, simple) MVC apps, then all of this is pretty much done in the same project (I'm talking about Visual Studio Project in this case), and the separation between the MVC "Layers" is pretty much just folders in the VS project (well separate namespaces).

With some of our other projects we have adopted a Service -> repository style, so this one is not going to be any different.

We are using Entity Framework as the DB persistence (DB first approach).

We have separated our DB access (the EF stuff) into another VS project, so we have a Web project and Models (or data) project in the solution.

The web project has the controllers and views, and the data project has the Services, Repositories and the EF stuff.

My confusion is with the models (or perhaps with understanding a Domain Model vs a View model)

If I was to try to follow the methodology (I think), I would have a domain model (the model that the EF and repository layers deal with), and then I would have a view model? (the model that the Controller and view would deal with), now wouldn't these be 90% the same? Isn't this way of separating out the concerns just making you write model code twice? As I am sure I read somewhere that Controllers and Views shouldn't have the Domain model?

One way that we have approached it is the EF makes all its model classes partial. We then extend that same class and add a MetaDataType class to it to make the 'View Model' (add the DataAnnotations to the properties) and then in essence the same model is passed through all layers, but is this 'best' practice (there is a splinter in my mind that this is just not right)

eg

[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
}

Maybe this is the 'best' way to attack it but I have not come across this method before.

We are creating all the Services and Repositories as Interfaces and using structure map as the IoC container. Another thing I admit, even though we are using the dependency Injection I am still struggling to come to terms with TDD, feels like to have to write everything twice (whole point of the DI I would think)

I suppose ultimately I am appealing to the willing here at SO that know more than me about architecting large ASP.NET MVC applications for some assistance and guidance. There seems to be a huge wealth of information out there, but all seems to be very conceptual. When I finally come to the implementation I get lost in the concepts.

EDIT

In response to Mr Karl Anderson

  • Paging of data in a view - Yes completely agree where this is where a viewmodel is pertinent and makes sense, but again is the CategoryListViewModel which has a List property, is it a list of the viewmodel category or the domain model category
  • Mass assignment vulnerability - I would think that this vulnerability would exist with a domain model or a view model (after all how will you set IsAdmin if genuinely needed to be set, surely it would still be on the ViewModel). I would think this would need to be dealt with at a different layer i.e. authorization, so that only uses of a curtain role can only set the IsAdmin
  • Displaying view information in a specific format - Surely this is just to do with model binding and/or view html helpers for formatting - i.e. a view and model binding issue only. After all, all models that are rendered through a view, have their properties end up in html and are all string at this point, so returning values have to be parsed anyway, the main principle of model binding, so if I needed a custom one, just write a new model binder.
  • Using your domain model as more than just data transfer objects (DTOs) - I actually try to avoid this as much as possible, trying to stick with the fact that models are exactly that, DTOs. But if that scenario came up I would probably write an extension method on the domain model, after all methods don't get serialized anyway, or yes add a view model but it would probably contain a domain model
  • Having different abstractions of the same domain model information - Agree in part. I would have a PagedAccountListViewModel (would still contain Domain models) but I would only use one model for the new and update account (I treat a new the same as an update, ones just prepopulated) and it would be the domain model
4

4 回答 4

8

当我最终开始实施时,我迷失在这些概念中。

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

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

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

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

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

EF 将其所有模型类设为部分。然后我们扩展同一个类并向它添加一个 MetaDataType 类以制作“视图模型”

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

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

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

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

于 2013-09-06T03:28:35.767 回答
7

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

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

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

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

背景

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

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

通常,N层的结构如下:

          entity

        interfaces

DataAccess  |  BusinessLogic

           UI

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

实体复制

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

public class OperationMessage{
    public bool IsError{get;set;}
    public string OperationMessage{get;set;}
}

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

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

  3. 继承OperationMessage

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

服务重复

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

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

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

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

这值得么?在某些情况下,是的。在其他情况下,例如企业应用程序,通常它更加僵化,并且不太可能发生迁移。但是,大多数时候企业可以扩展并且需要移动性。所以,动物园管理员必须成为护林员

我的 2 美分

于 2013-09-06T13:13:49.557 回答
3

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

于 2013-09-06T04:34:16.913 回答