一些管理臃肿控制器到业务服务层的Asp.NET MVC2最佳实践

6
我的控制器越来越大,失去了控制。
典型的控制器会执行以下操作:
- 确定给定用户是否有权访问给定资源。 - 验证ViewModel。 - 将ViewModel转换为DTOModel以进行持久化。 - 调用存储库以更新/创建新对象并关联其他新对象。 - 访问多个存储库助手类中的数据。 - 检查用户是否收到通知。 - 调用帮助程序发送电子邮件。 - 通过其他存储库对象将数据记录到数据库中。 - 等等...
简而言之,它们协调了很多事情。我想把所有东西都移到服务层中,但是没有看到我喜欢的代码示例中实现的模式。我已经查看了一些开源项目,如KiGG、Oxite、codecampserver等,但它们都没有真正解决缩小我的控制器的问题。我想避免传递太多HTTPContext的内容,但也许这是不可能的。
还有一些其他的项目和最佳实践可以参考吗?我正在构建一个大型的工作流/数据输入应用程序。
感谢提供链接和建议。
5个回答

3
我记不清楚有没有真正的例子可以展示,因为我认为我根据在SO上浏览随机问题和答案得出了我的MVC应用程序的控制器->服务->存储库分层方案。
然而,我可以给你一个如何将您列出的项目组织成我已经构建好的服务层结构的示例。这可能不是最好的方法,但这是我在我的大型mvc应用程序中所做的方式。这应该让你了解如何在你自己的应用程序中进行结构化。
我的服务层每个业务单元合并一个服务类。因此,如果我的应用程序有项目,并且每个项目都有一个人,我会有一个ProjectService类和一个PersonService类。
根据您对控制器工作方式的描述,我的控制器操作如下。
1)获取当前用户信息并调用适当的服务类的授权方法。因此,如果用户尝试修改项目的详细信息,我将向ProjectService的AuthorizeUser方法传递用户ID和项目ID。这意味着如果我更改授权用户项目的方式,我只需更改授权方法,而无需更改每个控制器。

2) Viewmodels在服务层中创建、保存和销毁。服务层接收viewmodel,验证它(如果失败,则引发异常或验证结果),然后将其转换为数据对象,并将其传递给存储库进行保存。它还从存储库请求数据对象,将其转换为viewmodel,并将其返回到控制器。

3) 所有操作的日志记录都在服务层中进行。这可以根据所呈现的操作自动完成(尝试将对象保存到数据库)或者您的控制器可以显式调用服务层来记录操作。

整个目的是将常见功能合并到易于维护的层中。如果更改viewmodel转换为DTO的方式,非常容易知道在哪里进行更改并仅进行一次更改。如果需要更改日志记录、用户访问权限,甚至如果要更改如何从存储库检索某些数据,则只需更改一个简单区域,而不必寻找所有控制器并直接修改它们。

编辑:这使得控制器很小,因为它们只包含对服务层的几个调用,就这样(授权、执行操作、显示视图)。结束编辑

最后,asp.net网站有一个关于在服务层执行验证的教程。该教程可以在这里找到。

我正在考虑这个答案。我喜欢服务类授权的想法。 - taudep
刚刚看到你编辑添加的链接,非常有帮助。感谢您的发布。 - taudep
通过你发送的链接文章,他们正在使用旧式的AddModelError调用进行验证。我想知道是否有一种方法可以将较新的数据注释验证纳入其中。(尽管,我编写的某些验证规则无法映射到属性。) - taudep
基于服务的授权还允许更容易地进行单元测试。 - KallDrexx
到目前为止,我还没有找到一种好的方法来验证DataAnnotations而不使用MVC框架。很遗憾,虽然它不在MVC命名空间中,但我相信肯定有一种方法。但几乎所有关于DA的参考都是关于MVC的,包括那些谈论如何无法单元测试数据注释的文章。如果你找到了一种方法,我肯定会很想知道。 - KallDrexx
http://www.codeproject.com/KB/validation/Data_Annotations_with_ASP.aspx 看起来似乎可以用于验证没有MVC的注释。它看起来本质上是循环遍历所有属性,获取属性的所有验证属性,并对其运行IsValid()。 - KallDrexx

3

1

我觉得你的控制器负责太多的角色了。

在设计控制器时,我通常认为它应该控制对单一类型资源的访问。例如,如果我正在创建您正在阅读的页面,您可以在此页面发布答案和评论,则会有两个控制器,一个用于回答,另一个用于评论。我会避免将两个与资源访问无关的操作添加到我的问题控制器中。当然也有例外,但是很少。

此外,每个控制器的基本功能都应该验证输入(即使在浏览器中进行验证,因为任何人都可以编辑请求),将输入转换为传递给服务层(或业务逻辑)所需的对象,并验证服务层的响应,然后将其转换回可由视图使用并返回给用户的对象。其他所有内容都应在服务层处理,保持控制器瘦和可预测性。


嗯...是的,在每个操作中,控制器正在执行许多任务。然而,无论如何,每次请求都必须完成所有任务。这就是为什么我正在寻找更好地将其拆分为服务等的方法。我并不想将控制器的不同操作拆分为控制器上的多个操作,而是要将冗长的单操作方法拆分为更好的部分。 - taudep
我意识到这些事情确实需要在每个请求上发生,但它们不必由控制器本身完成。例如,如果决定向用户发送电子邮件的依据是从存储库返回的数据,则可以添加一个中间层,该层将输入传递给存储库并在将数据返回给控制器之前执行检查。只要您有意识地将每个对象调节到特定和恒定的责任集,分解工作通常会自然而然地进行。 - Nick Larsen

0

我们公司一共有5个项目:

  • View(视图)
  • Controller(控制器),我们只在这里放置动作方法的逻辑和其帮助程序
  • Business Logic(业务逻辑),我们在这里放置操作的具体逻辑。例如,对于电子邮件的帮助程序、业务验证,以及调用存储库来更新和创建对象的地方
  • Data Access(数据访问),我们在这里放置查询和数据操作的存储库。
  • ORM(对象关系映射),我们在这里放置数据库模型和在每个层之间交换数据的类

在这种情况下,所有项目都引用了ORM,然后View引用了Controller,Controller引用了Business Logic,而Business Logic则引用了Data Access。


1
我无法看出这甚至尝试解决如何防止控制器大小爆炸的问题。 - Nick Larsen
从我的角度来看,将一些逻辑放在另一个层中会减小控制器的大小。 为最常见的操作创建一个基类也会有所帮助。 - Tejo
我正在寻找有关“其他层”业务逻辑层的更多详细信息,例如样例设计、模式等。 - taudep
我们这里的情况其实非常简单,但是我们的项目似乎没有你们的那么大。我们的业务逻辑只与一个实体或模型对象相关联,并且它们仅处理与这些绑定对象相关的操作,如果需要,它们可以相互通信,但不处理除泛型参数中定义的类型之外的其他类型。我不知道这对你们的项目是否可行,但这样做可以使代码更有组织性。 - Tejo

0

好的,这部分内容有些取决于你所说的“控制器是什么”。如果你使用数据注释进行验证和操作筛选器进行日志记录,那就没问题了。控制器中没有任何逻辑。你是否使用ViewModels和强类型视图?看起来你的控制器正在处理的模型应该已经是简化的DTO,而不是拥有完整实体并创建DTO发送回去。我很难想象一个控制器会发送电子邮件或检查它是否发送成功。这应该交给服务层处理。我还会仔细查看操作“关联对象”的控制器。它可能应该调用存储库上的单个方法来处理它。

我没有打开Nerd Dinner,所以我不能保证里面有什么,但值得研究一下。


是的,我正在使用简化的ViewModels来对UI中的数据进行建模。问题在于,视图模型会被拆分成多个数据对象以进行持久化。这个过程中有很多复杂的规则,很多授权检查等等...我发现Nerd Dinner在实际应用中缺乏任何体面的最佳实践。毕竟,它基于一个只有两个表的数据库。它不处理多对多关系,或者任何复杂的数据关系。我经常有业务逻辑涉及到单个Web请求中的10个表。 - taudep

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接