65.9K
CodeProject 正在变化。 阅读更多。
Home

架构指南:ASP.NET MVC 框架 + N 层 + Entity Framework 等等

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (214投票s)

2010年4月1日

CPOL

40分钟阅读

viewsIcon

1368124

downloadIcon

38468

如果您想使用 ASP.NET MVC 框架,但又在为如何组织项目以自信地用于下一个业务项目而苦恼。那么这篇文章就是为您准备的。本文将指导您如何使用 ASP.NET MVC 框架来架构一个小型文档管理系统。

下载 MvcDemo.zip - 2.02 MB
推荐框架: Nido Framework

目录

引言

模型-视图-控制器(MVC)是一种在软件工程中使用的架构模式。该模式将“领域逻辑”(用户应用程序的逻辑)与输入和表示(GUI)隔离开来,从而允许独立地开发、测试和维护每个部分。

如今,微软的 MVC 变体 ASP.NET MVC(现在是 .NET Framework 4.1 的一部分)在软件设计师中越来越受欢迎。此外,开发人员也热衷于通过提供加速应用程序开发的材料来支持社区使用 ASP.NET MVC 框架。互联网上有很多资源库,可以在其中找到此类贡献。这些免费的现成库/组件以多种方式对现代软件行业产生了积极影响。然而,与任何其他情况一样,这也有一些负面影响。如今,不幸的是,那些试图在 MVC 中实现第一个系统的初级设计师,正在努力正确地选择一套现成的组件。他们通过不必要地承诺使用这些现成的组件,而设计出非常复杂的、不必要的复杂的设计。人们需要理解,您不必在每个(或第一个)系统设计中使用所有东西。您只需要使用适合的。但在提供许多不同选项时,找到合适的选项并非易事。

所述问题比之前陈述的要复杂一些,因为技术提供商也有竞争性产品。因此,首先,我将举几个例子,展示达到相同目标的几种不同方法。

  1. Entity Framework、Linq to SQL、Subsonic、Hibernated 或其他任何都可以用作 ORM(对象关系映射)工具。
  2. 可以使用*'AutoMapper'*、*'StructureMap'* 或其他任何工具将一个 DTO(数据传输对象)映射到另一个。
  3. 可以使用微软企业库的*'Validation Application Block'*、.NET Framework 的*'System.ComponentModel'* 命名空间、.NET Framework 的微软工作流引擎(WF)或任何其他工具来验证业务对象。
  4. MVC 的 Views、Web-Form 或通用的 ASP.NET 控件,甚至纯 HTML 都可以用来开发用户界面(UI)层。
  5. JavaScript、Ajax、J-Query 可以作为您的前端脚本语言,可以与 JSON(JavaScript 对象表示法)一起使用,也可以不使用。
  6. MVC 架构或其他任何架构,甚至架构的组合都可以用来设计一个系统。

在上面列出的项目中,您可以看到存在相似或更适用的选项。此外,还有其他选项,必须根据您的需求进行选择。这些选项必须在考虑系统的功能和非功能性需求后进行选择。一旦设计得当,系统就能真正为您的开发过程增加价值。经验丰富的设计师会第一次就正确选择选项,从而获得显著优势。如果您是初级系统设计师,那么至少要小心不要选择一套糟糕的错误选项。这样的选择可能会让您的“软件设计”生涯在你开始之前就已经结束。

他们说"完美(在设计中)并非在无事可加时达到,而是在无事可减时达到。"

阅读本文的人会认识到,一个架构是用 Microsoft ASP.NET MVC Framework、Microsoft Entity Framework、Ajax、J-Query、*'AutoMapper'*、Enterprise Library (Unity Application Block)、*'MsTest'*、Log4Net 等构建的。此设计可能不是最完美的设计。但我可以向您保证,它也不是一个极其错误的设计。它将属于可接受的范围,一旦完成,您就可以将其作为框架来重用,以开发您自己的(中小型)Web 应用程序。

先决条件

为了跟上本文的进度,您应该至少有4年以上的开发经验,并对 OOP 概念和设计模式有深入的理解。此外,由于我们的系统将使用 ASP.NET MVC 框架进行设计,因此您需要有开发一些 ASP.NET MVC 框架的测试应用程序的经验,最好是使用 Microsoft Entity Framework。如果您认为自己具备足够的专业知识,那么最好继续阅读本文。

如果您还没有设置 ASP.NET MVC,请在继续之前安装以下列出的项目。您也可以通过 Microsoft Web Platform Installer 来安装 ASP.NET MVC。

什么是 ASP.NET MVC?

如前所述,ASP.NET MVC 是 MVC 的微软变体,它是一个免费的微软框架,用于使用模型-视图-控制器模式开发出色的 Web 应用程序。它提供了对 HTML 和 URL 的完全控制,支持丰富的 Ajax 集成,并促进了测试驱动开发。 了解更多 >>

我希望上述描述也能回答一些开发者提出的著名问题,“MVC 是模式还是框架?”。让我再次重申,以确保您正确理解。MVC 既是一种模式,也是一个框架。MVC 模式被用于开发 Microsoft ASP.NET MVC 框架。因此,首先引入的是模式,然后是框架。

为什么选择 ASP.NET MVC?

ASP_NET_MVC_WITH_EF/MVCSummary.jpg

ASP.NET 并不是全新的东西,而是微软在标准 ASP.NET 库之上构建的东西。它可以被视为 .NET Framework 的扩展。换句话说,它比标准的 ASP.NET 或 Web Forms 更强大。在使用 Web-Form 时,框架本身控制着许多变量,而在 ASP.NET MVC 中,则由您自己控制。因此,使用 ASP.NET MVC,您可以决定如何构建系统。

尽管我说在使用 ASP.NET MVC 时需要照顾一切,但实际上并不难。互联网上有许多资源可以使 MVC 开发人员的生活更轻松。与过去抱怨为了在 MVC 中完成某项工作而需要编写更多代码不同,现在借助其他开发人员的贡献,您可以更快地完成它们。‘MVC Contrib’ 就是其中之一,它旨在为微软的 ASP.NET MVC 框架添加功能和易用性。对于希望在 ASP.NET MVC 框架之上开发和测试 Web 应用程序的开发人员来说,*'MVC Contrib'* 非常有用。

此外,ASP.NET MVC 框架的最新版本还提供了许多新功能。您可以通过阅读 www.asp.net/mvc 网站上的“What’s New in ASP.NET MVC 2”文档来了解更多信息。

ASP.NET MVC 是唯一的选择吗?

存在争论。一些设计师认为 Web-Form 比 MVC 更好。他们说,Web-Form 已经内置了许多第三方控件。MVC 是可扩展的,但他们都说该架构需要您编写更多的代码。此外,他们还说,一个设计良好的 Web-Form 应用程序也可以像 MVC 一样具有可扩展性。

在我看来,决策应该基于您的需求。当势均力敌时,最新的技术是更好的选择。然而,我们不应该仅仅因为我们对改变习惯的自然抗拒而选择 Web-Form。这种抗拒改变的习惯在 IT 行业是无法实践的。IT 行业变化迅速,迫使人们放弃旧事物,拥抱新事物。我认为 ASP.NET MVC 将拥有一个光明的未来。微软这次在 ASP.NET MVC 上做得很好。它已经证明是构建下一代 Web 应用程序的框架。所以,为什么还要等待?如果您早期采用,您将更容易随之成长。否则,随着新一波技术更新的到来,您将体验到成为过时开发者的痛苦。

在文章的下一部分,我将逐步开始设计我们的文档管理系统(DMS)。这将展示 Microsoft ASP.NET MVC 框架在构建真实 Web 应用程序中的应用。不过,在继续之前,我需要在此强调一些关于 MVC 的重要事项。

  • ASP.NET MVC 不是“WebForms 4.0”
  • MVC 有一个小小的学习曲线,但有*很多*资源可以帮助您
  • MVC 让您拥有更多控制权,这通常是更好的
  • 如果您喜欢单元测试,ASP.NET MVC 将会让您满意。

使用 ASP.NET MVC 构建 DMS

我仔细选择了一些选项来构建我们的 DMS。该设计利用了 Microsoft ASP.NET MVC Framework、Microsoft Entity Framework、Ajax、J-Query、*'AutoMapper'*、Enterprise Library (Unity Application Block)、*'MsTest'*、Log4Net 等。我计划在本文稍后讨论我做出的一些重要选择,但大部分我希望您通过查看源代码本身来学习。我将主要专注于我们的目标,即教您如何建立正确的架构来使用 ASP.NET MVC 框架构建小型到中型 Web 应用程序。在此过程中,我计划触及所有重要点。我将提供足够的信息供经验丰富的开发人员理解,但不会更多,因为我需要控制文章的长度。

作为架构我们 DMS(或任何)系统的第一步,您需要彻底分析系统。这包括充分深入地理解其需求、时间周期、成本限制和质量要求等。下面我给出一些有助于您理解的要点...

  • 拟建系统旨在解决的业务问题是什么?
  • 功能和非功能性需求
  • 项目质量要求
  • 客户愿意为该系统花费多少钱?
  • 项目周期,项目开始和结束
  • 规模(系统有多大?)
  • 系统需要多灵活?
  • 系统需要多易于扩展?
  • 系统需要多易于定制?

一旦完全理解了系统需求,主要涵盖以上问题,您就可以开始设计系统的最初几步。在这里,您定义软件的结构,并找到该结构为系统提供概念完整性的正确方式。这是开发工作产品,它在质量、进度和成本方面都能带来最高的投资回报。

设计考量

软件设计有许多方面需要考虑。每个方面的重要性应反映软件试图达成的目标。以下是我从 Wiki-Software Design 中引用的其中一些方面...

  • 兼容性 - 软件能够与其他旨在与另一产品互操作的产品一起运行。例如,软件可能与旧版本兼容。
  • 可扩展性 - 可以在不大幅更改底层架构的情况下为软件添加新功能。
  • 容错性 - 软件能抵抗并从组件故障中恢复。
  • 可维护性 - 软件可以在指定的时间内恢复到指定的状态。例如,防病毒软件可能包含定期接收病毒定义更新的功能,以保持软件的有效性。
  • 模块化 - 生成的软件由定义良好、独立的组件组成。这有助于提高可维护性。然后可以在集成形成所需的软件系统之前,独立地实现和测试组件。这允许在软件开发项目中划分工作。
  • 包装 - 印刷材料,如包装盒和手册,应与目标市场指定的风格相匹配,并应提高可用性。所有兼容性信息都应显示在包装外部。应在包装内包含使用所需的所有组件,或在包装外部指定为要求。
  • 可靠性 - 软件能够在指定条件下执行所需功能,持续指定一段时间。
  • 可重用性 - 设计的模块化组件应捕获它们预期的功能本质,不多也不少。这种单一的目的是使组件在其他设计中有类似需求时可重用。
  • 健壮性 - 软件能够在压力下运行或容忍不可预测或无效的输入。例如,它可以设计为具有对低内存条件的韧性。
  • 安全性 - 软件能够抵御敌对行为和影响。
  • 可用性 - 软件用户界面必须对其目标用户/受众可用。参数的默认值必须选择得当,以便对大多数用户来说是好的选择。

想象一下,我们的 DMS 是一个复杂的业务问题,然后尝试通过抽象和关注点分离来指导您的设计工作,以降低其复杂性。这必须通过将系统分解为其结构化元素、架构组件、子系统、子组件、零件或*'块'*来实现。这并不难。任何有经验的设计师只需查看需求文档就可以识别属于某个系统的组件。但是,有一些标准技术也可以做到这一点。我在这方面也写了一篇文章,您可以在 Code Project 上找到,标题是"计算机系统设计与架构的实用方法"。

您如何架构一个系统?

一个经验丰富的架构师不需要走完书中的每一个步骤就能为一个小型 Web 应用程序完成一个合理的设计。这样的架构师可以利用他们的经验来加快流程。由于我以前做过类似的 Web 应用程序并且了解我的交付成果,我将采取更快的途径来完成我们 DMS 设计的初始部分。这有望帮助我缩短本文的篇幅。

对于没有经验的人,我将在下面简要说明架构软件所涉及的通用步骤...

  1. 理解初始客户需求 - 提出问题并进行研究以进一步阐述需求
  2. 定义系统的流程,最好是可视化(图表)形式。我通常在这里绘制流程图。在此过程中,我会先尝试定义系统的手动版本,然后尝试将其转换为自动化版本,同时识别流程及其关系。我们在这里绘制的流程图也可作为与客户验证捕获需求的中介。
  3. 确定适合您需求的软件开发模型
    • 当在设计开始之前完全捕获和定义了需求时,您可以使用*'瀑布'*模型。但是当需求未定义时,可以使用*'螺旋'*模型的变体来处理。
    • 当需求未定义时,系统在设计过程中被定义。在这种情况下,您需要在相应模块中留出足够的空间,以备将来扩展。
  4. 决定使用什么架构。在我的情况下,为了设计我们的文档管理系统(DMS),我将使用 ASP.NET MVC 和 多层架构(三层变体)的组合。
  5. 分析系统并识别其模块或子系统。 
  6. 一次选择一个子系统,进一步分析它,并识别属于系统该部分的的所有细粒度需求。
  7. 识别数据实体并定义实体之间的关系(实体关系图或 ER 图)。然后可以识别业务实体(某些业务实体直接映射到您系统的类)并定义业务流程。
  8. 组织您的实体。这是您规范化数据库,并决定使用哪些 OOP 概念和设计模式等的地方。 
  9. 使您的设计保持一致。跨所有模块和层遵循相同的标准。这包括简化概念(例如,如果您在两个不同的模块中使用了两种不同的设计模式来实现相同目标,那么选择更好的方法并在两个地方都使用它)以及项目中使用的约定。
  10. 调整设计是过程的最后部分。为此,您需要与项目团队举行会议。在会议中,您需要向团队展示您的设计,让他们就此提出问题。将此视为诚实评估/调整设计的机会。

为我们的 DMS 构建代码框架

正如我所说,我们的文档管理系统(DMS)架构是通过结合三层架构和模型-视图-控制器架构实现的。

为了加快流程,我将直接定义我们 DMS 的系统框架。请按照以下说明在 Visual Studio 2010 中直接创建您的系统框架。

  1. 打开您的*'Visual Studio 2010'*,创建一个 ASP.NET MVC 类型的新项目(在我的例子中是 MVC 版本 2.0)
  2. 在同一个解决方案中,为您的数据访问层(DAL)创建一个*'控制台库'*类型的项目。这将为您的数据访问层创建一个单独的程序集。除了在逻辑上将您的数据访问逻辑分组到一个单独的程序集中,这种分离还具有以下优点:
    • 允许在不影响其他层的情况下修改数据访问逻辑。
    • 支持轻松扩展系统。
    • 支持在不影响相应业务功能的情况下更改数据库类型。
    • ...等等。
  3. 为核心业务操作创建一个*'控制台库'*类型的项目。与 DAL 一样,这将为业务逻辑创建一个单独的层。
  4. 创建另一个*'控制台库'*类型的项目用于通用操作。有些业务功能对所有层都是通用的。这些函数可以实现在单独的程序集中。这样您也可以在其他系统中使用它。我稍后会在文章中讨论这一点。

完成后,您的解决方案看起来将是这样的。

ASP_NET_MVC_WITH_EF/ProjectList.jpg

在上图中,*'MvcDemo'* 项目是 MVC Web 项目。该项目是使用 Visual Studio 的默认 MVC 项目模板创建的。在该项目中,我决定将*'Views'* 和*'Controllers'* 保留在同一个 Web 项目中,而*'Models'* 被移出了另一个层(您稍后会详细了解此更改)。所以总而言之,现在您有一个解决方案,其中包含一个 ASP.NET MVC Web 应用程序项目,三个空的控制台库以及一个 Visual Studio 自动为我们创建的测试项目。

注意:我使用了主项目名称('MvcDemo')作为控制台库名称的前缀,但这您可以选择遵循或不遵循。

在我们的解决方案中,每个子系统代表一个项目。每个项目代表主业务问题的一个有意义的部分。如果您想在多个项目中使用一个项目(就像我们将要使用*'MvcDemo.Common'* 项目那样),您可以通过添加项目引用来实现。

让我们看看我们的数据访问层设计

数据访问层提供了一个集中位置,用于所有到数据库的调用,从而更容易地将应用程序移植到其他数据库系统。有许多不同的选项可以快速构建数据访问层。

在构建 DAL 的过程中,我将从许多不同的模型提供商中选择,例如

  1. NHibernate
  2. LINQ to SQL

或者,我也会选择其他模型提供商,可能包括

  1. SubSonic
  2. LLBLGen Pro
  3. LightSpeed
  4. ...或者谁知道呢

这意味着我们将使用 ORM(对象关系映射)工具来生成我们的数据访问层。正如您所见,有很多工具,但都有其优缺点。为了选择正确的工具,您需要考虑您的项目需求、开发团队的技能,以及组织是否标准化了某个特定工具等。

但是,我将不使用上面列出的工具,而是决定使用 Microsoft Entity Framework(EF),这是微软新推出的一种技术。ADO.NET Entity Framework (EF) 是 .NET Framework 的一个对象-关系映射 (ORM) 框架。ADO.NET Entity Framework (EF) 抽象了存储在数据库中的数据的关系(逻辑)模式,并向应用程序呈现其概念模式。然而,EF 创建的概念模型未能满足我的要求。因此,我必须找到一种变通方法来为此设计创建正确的概念模型。您将在本文稍后部分看到我为实现我的要求所做的一些调整。

为我的 ASP.NET MVC 前端选择 EF(Entity Framework)并非盲目决定。我有一些信心...

  1. ASP.NET MVC Framework 是微软的产品,ADO.NET Entity Framework 也是。它们之间的协同工作比任何其他工具都更好。
  2. EF(Entity Framework)的最新版本具有许多有前景的功能,并已解决了其早期版本中的许多问题。
  3. 总的来说,微软似乎对 EF 很认真。这让我认为 EF 在 ORM 工具的未来中将占有重要份额。

社区对 EF 的承诺与实际交付之间存在担忧。您可以阅读此 ADO .NET Entity Framework 投票不信任 以获取更多详细信息。微软和 Entity Framework 团队从 .NET 平台上的实体应用程序和软件架构专家那里获得了大量反馈。尽管微软宣布提供实体架构框架支持的意图受到了热烈欢迎,但 Entity Framework 本身一直以来都引起了人们的极大担忧。然而,Entity Framework 的第二个版本(名称有些令人困惑,称为*'Entity Framework v4'*, 因为它是 .NET 4.0 的一部分)以 Beta 形式作为 Visual Studio 2010 的一部分提供,并解决了版本 1 的许多批评。

下面给出了 ADO.NET Entity Framework 为我们的 DMS 自动创建的实体数据模型(也称为数据库的概念模型)。这在数据库表和所谓概念模型之间存在 1:1(一对一)映射。这些模型或对象可用于与各自的物理数据源或数据库表进行通信。我希望您能轻松理解我的概念模型。

ASP_NET_MVC_WITH_EF/EntityDesignerDiagram.jpg

下面的数据访问层项目包含三个文件夹:Models、Infrastructure 和 Interfaces(您稍后会注意到,此项目模板在系统的其他项目中也会被重新使用)。在这些文件夹之外,您会发现一些类,例如一个基类和两个具体的类。这些是数据访问层(DAL)项目的主要操作类。此项目模板使得无需导航到目录即可直接访问所有常用的主要操作类。

"软件系统的固有复杂性与其试图解决的问题有关。实际的复杂性与构建的软件系统的大小和结构有关。两者之间的差异衡量了解决方案与问题不匹配的程度。"

-- Kevlin Henney,“为了简单起见”(1999)

简洁是效率的灵魂。这并不意味着设计必须不切实际地简单。它必须刚好足以实现需求,不多也不少。此外,一致性也很重要。它可以减少模块之间的差异,从而有助于轻松理解设计。因此,设计必须简单而一致。牢记这些,让我们仔细看看我们的数据访问层(DAL)设计。

ASP_NET_MVC_WITH_EF/DalDesingView1.jpg

在数据访问层(DAL)设计中,我考虑使用一种存储库模式的变体。存储库模式通常被许多企业级设计师使用。它是一种直接的设计,为您提供可测试和可重用的代码模块。此外,它还提供了灵活性以及概念模型和业务逻辑之间的关注点分离。在我的设计中有两个存储库,分别是‘DocumentRepository’和‘FileRepository’,它们将通过领域对象(也称为业务对象或模型)在域(也称为业务逻辑层)和数据映射层(也称为数据访问层)之间进行协调。

通常建议每个业务对象都有一个存储库类。

在项目中看到的文件夹中,名为 'Interfaces' 的文件夹很重要。所以,让我们检查一下 'Interfaces' 文件夹,看看里面有什么接口...

  1. IRepository<T> - 这是通用接口,抽象地定义了我们系统中所有存储库的行为。这是超级存储库。它被扩展以创建名为 'RepositoryBase<T>' 的抽象存储库。
  2. IDocumentRepository – 这是一个专门的存储库,它定义了与*'Document'* 特定的行为。
  3. IFileRepository – 就像 'IDocumentRepository' 一样,它定义了*'File'* 存储库的行为。
  4. IRepositoryContext - 它定义了我们存储库上下文的行为。
  5. IPagination - 它为分页相关操作定义了定义。
  6. *'IUnitOfWork'* 和 *'IUnitOfWorkFactory'* 最初存在,但后来从设计中移除。起初,我计划编写一些依赖注入(DI)的代码。但后来我发现微软企业库已经为我们完成了所有工作。它们有一个称为*'UnityApplicaitonBlock'* 的应用程序块,它实现了我最初通过这两个接口想要实现的功能。因此,我从项目中删除了 unity 和控制反转相关的实现。现在您下载的源代码中将没有这些实现。

我在 DAL 设计中使用了接口,这可能会让您问“为什么我们需要这些接口?”和“为什么不能只有具体实现?”。在设计中包含接口有很多原因。它们允许替换实现(一个常见场景是,在单元测试时,您可以将实际的存储库实现替换为假的实现)。此外,如果使用得当,它们有助于更好地组织您的设计。

接口是解释函数或类将做什么所需的最低限度。它是编写完整功能和可用性声明的最少代码量。因此,它(对您的函数或类的使用者来说)更清楚地解释了对象的工作原理。用户不应该需要查看您所有的实现来理解对象的作用。所以,再次定义接口是一种更有条理的方法。

在某些情况下,我看到设计师为每个类都添加接口,我认为这是一种过于极端的实践。在我的设计中,我使用接口不仅是为了组织设计,也是为了替换实现。稍后在文章中,您将看到这些接口如何用于协助“unity application block”注入依赖项。

您可以看到,在上面的屏幕截图中,我还为分页定义了一组接口。分页(业务对象/实体)是开发人员在许多不同层中编写的功能。有些人在 UI(用户界面)层编写,而另一些人在 BLL(业务逻辑层)或 DAL(数据访问层)编写。我考虑将其实现到 DAL 中,以避免在分布式多层部署设置中可能发生的非必要的网络往返。此外,将其保留在 DAL 中,我将有机会使用一些内置的 EF(Entity Framework)函数来进行分页工作。

下图总结了整体 DAL 设计及其主要组件和它们之间的关系。我认为清楚地说明了三个接口及其具体实现是如何定义的。

ASP_NET_MVC_WITH_EF/ClassDiagram.Dal1.png

我认为了解此设计如何演变很重要。所以,让我在此讨论一下。最初,我有一个*'IRepository<T>'* 接口。它用于实现两个专门的‘Document’和‘File’存储库。那时我注意到这两个存储库有许多共同的操作。所以我想用一个抽象类来抽象地定义它们。因此,决定添加一个名为 'RepositoryBase<T>' 的新通用抽象类。在完成所有这些之后,最终的系统在顶部有一个通用的*'IRepository<T>'* 接口,然后是抽象的 'RepositoryBase<T>' 类,然后是 'File' 和 'Document' 存储库的两个具体实现。我认为已经完成了,在关闭设计之前又看了一眼,但这让我意识到设计仍有问题。所以,我将在下一段中讨论。

我所做的设计使得无法替换任一专门存储库的实现。例如,如果我有一个名为 'TestDocumentRepository' 的 'DocumentRepository' 的测试/模拟版本,并想将其替换为 'DocumentRepository',那么这在我的设计中是不可能的。所以我决定通过引入两个专门的接口‘IDocumentRepository’和‘IFileRepository’来对设计进行一些调整。这使得完全隐藏特殊实现成为可能,从而获得所需的互换性。这个最终的更改完成了我的 DAL 设计。

正如我之前所说,我使用 unity application block 将依赖注入到此系统中。下面的代码显示了如何使用 unity application block 来替换 IDocumentRepository 的实现。Unity Application Block 支持在 'global.asax' 文件中编写此代码,或使用 'web.config' 设置解析依赖项的详细信息。请稍后期待更多详细信息...

//when using the actual DocumentRepository implementation
.RegisterType<IDocumentRepository, DocumentRepository>()

//when using the test version of the DocumenRepository implementation
.RegisterType<IDocumentRepository, TestDocumentRepository>()

我还意识到,经过一些调整,这个完全相同的设计模式也可以在业务逻辑层中使用。因此,您将在*'MvcDemo.Core'* 项目中看到相同的*设计方法*被重用。

在继续下一部分之前,让我们看一下*'RepositoryBase<T>'* 类的实现。在基存储库实现中,我小心地避免使其变得不必要地复杂。如您所见,它只有最基本的方法。您还可以看到该类如何通过构造函数传递依赖的*'IRepositoryContext'* 来处理其依赖关系。稍后您将看到如何使用此技术跨所有层注入依赖项。您可以在 这里 找到更多关于 Unity DI(依赖注入)的信息。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Objects;
using System.Linq.Expressions;
using MvcDemo.Dal.Interfaces;
using MvcDemo.Dal.Infrastructure;
using MvcDemo.Dal.EntityModels;

namespace MvcDemo.Dal
{
    public abstract class RepositoryBase<T> : IRepository<T>
        where T: class
    {
        public RepositoryBase()
            : this(new DmsRepositoryContext())
        {
        }

        public RepositoryBase(IRepositoryContext repositoryContext)
        {
            repositoryContext = repositoryContext ?? new DmsRepositoryContext();
            _objectSet = repositoryContext.GetObjectSet<T>();
        }

        private IObjectSet<T> _objectSet;
        public IObjectSet<T> ObjectSet
        {
            get
            {
                return _objectSet;
            }
        }

        #region IRepository Members

        public void Add(T entity)
        {
            this.ObjectSet.AddObject(entity);
        }

        public void Delete(T entity)
        {
            this.ObjectSet.DeleteObject(entity);
        }

        public IList<T> GetAll()
        {
            return this.ObjectSet.ToList<T>();
        }

        public IList<T> GetAll(Expression<Func<T, bool>> whereCondition)
        {
            return this.ObjectSet.Where(whereCondition).ToList<T>();
        }

        public T GetSingle(Expression<Func<T, bool>> whereCondition)
        {
            return this.ObjectSet.Where(whereCondition).FirstOrDefault<T>();
        }

        public void Attach(T entity)
        {
            this.ObjectSet.Attach(entity);
        }

        public IQueryable<T> GetQueryable()
        {
            return this.ObjectSet.AsQueryable<T>();
        }

        public long Count()
        {
            return this.ObjectSet.LongCount<T>();
        }

        public long Count(Expression<Func<T, bool>> whereCondition)
        {
            return this.ObjectSet.Where(whereCondition).LongCount<T>();
        }

        #endregion

    }
}

在上面的代码中,*'ObjectSet'* 是一个通过*'get'* 暴露的公共属性。这将允许外部方了解当前活动存储库的对象集。但是,当您使用特定类型实现存储库时,返回的*'ObjectSet'* 是可预测的。这是我设计中的一个弱点。例如,在实现‘IFileRepository’时,*'ObjectSet'* 保证返回*'IObjectSet<File>'*,而不是其他任何东西。因此,有人可能认为该属性没有必要公开暴露。这是一个非常有效的论点。正如您现在可以认识到的,我无法为我的设计辩护。一个人应该能够为自己的设计辩护。因此,我认为可以接受将属性的访问级别从*'public'* 降低到*'protected'*。这样,只有扩展类才能访问活动的对象集。正如我之前所说,您必须质疑您的设计,质疑才能验证设计。

如果您查看下面的代码行,您可能会注意到一些不熟悉的东西。*'??'* 操作符在一般编码中不常用,但在这里对我很有用。

repositoryContext = repositoryContext ?? new DmsRepositoryContext();

*'??'* 操作符称为 null 合并操作符,用于为可空值类型以及引用类型定义默认值。如果左侧操作数不是 null,则返回左侧操作数;否则返回右侧操作数。

这种代码模式在使用外部模块(Unity Application Block)注入依赖项时很重要。

让我们看看我们的业务逻辑层设计

业务逻辑层通常是我们多层架构中的一层。代码的这一部分包含我们应用程序的业务逻辑。将业务功能分离到单独的项目将具有我们在数据访问层设计中认识到的相同优点。

下面的屏幕截图扩展了系统的*'Business Logic Layer'* 部分。其中,名为*'Models'* 的文件夹包含一组*'View'* 和*'Edit'* 模型类。这些类代表 MVC 模式的*'Models'*。它们是我们 DMS 的 BLL(业务逻辑层)的域/概念模型。例如,要在前端显示文档,您可以有一个名为*'DocumentViewModel'* 的视图模型,其中包含要在 UI 上显示的属性。这些模型类也有其相应的*'Views'* 和*'Controllers'*。此外,当*'View'*(网页)用于编辑/更新实体时,您可以使用*'DocumentEditModel'* 来捕获用户输入。该模型也可用于存储其属性验证要求的定义(请参阅下面的‘验证业务对象’以获取更多详细信息)。

您可以进一步分解此系统以使其更灵活。但这需要付出代价。您分解得越多,维护代码就越困难。我认为这正好符合我们的要求。因此,根据您的要求,您可以决定需要进一步分解系统的程度。

您现在可以暂时忽略‘Mapping’相关的接口,但我稍后会回来讨论。

ASP_NET_MVC_WITH_EF/CoreProjectViewNew1.jpg

如您在上面的屏幕截图中看到的,BLL 设计在某种程度上与我们的数据访问层(DAL)设计相似。就像 DAL 一样,我有一组单独的接口来定义该层的框架。

  • IService<T> - 这是用于实现此应用程序的服务超接口。我还有一个名为‘IServiceBase’的接口,但这使得 BLL 与 DAL 设计不一致。因此,我决定稍后将其从设计中删除。
  • IDocumentService - 这定义了*'Document'* 特定的服务。
  • IFileService - 这定义了*'File'* 特定的服务。
  • IMapper, IMapperRegister - 我试图使映射库成为一个可插拔的库,这样我就可以稍后插入任何其他映射库。但是,您需要给我一点时间来完成这部分代码。

如您所见,我有一个实现*'IDocumentService'* 的*'DocumentService'*,以及实现*'IFileService'* 的*'FileService'*。*'ServiceBase<T>'* 是 DAL 的*'RepositoryBase<T>'* 的对应部分,目前是空的。然而,根据我的经验,我知道将来会有一些对 Document 和 File Service 都通用的函数,可以抽象地实现/定义在*'ServiceBase<T>'* 中。我认为,当您从一个通用接口实现多个实现时,定义一个抽象类型基类是一个好习惯。

ASP_NET_MVC_WITH_EF/ClassDiagramServiceNew1.jpg

让我们看看我们的表示层(用户界面)设计

这是我们应用程序的顶层。表示层向用户显示信息。它与其它层通信以生成结果到 Web 浏览器。

总的来说,我们使用的架构有几个不同之处,我想指出。我们将 MVC 的*'Models'* 从 Web 项目中移除,并将其包含在业务逻辑层中。我们还移除了三层架构的常规表示层,并将其与 MVC 前端合并。这意味着 MVC 的*'Views'* 和*'Controllers'* 代表了三层系统中的表示层部分。这有点复杂,我绘制了下面的图来进一步阐述。

ASP_NET_MVC_WITH_EF/MvcMergedThreeLayer1.jpg

在下面的屏幕截图中,您将看到我们的 ASP.NET MVC Web 项目结构是什么样的。ASP.NET MVC 应用程序的默认目录结构有 3 个顶级目录:

  • /Controllers
  • /Models
  • /Views

您可能已经猜到,它建议将 Controller 类放在 'Controllers' 目录下方,将 View 模板放在 'Views' 目录下方,并且您已经知道我们决定将 'Models' 移出此项目。所以,在我们的项目中,我们将不使用 'Model' 文件夹。

总的来说,ASP.NET MVC 框架并不强制您始终使用此结构。除非您有充分的理由使用替代的文件布局,否则我建议使用此默认布局。

ASP_NET_MVC_WITH_EF/WebProjectView.jpg

依赖注入

依赖注入是一种通过外部实体配置对象依赖项的方式。这听起来可能有点抽象,让我举个例子。

例如:在我们的代码中,有一个名为 'DmsController' 的类,它与 'IDocumentService' 有依赖关系。这意味着当您创建一个 DmsController 类型的对象时,您需要将*'IDocumentService'* 的一个实现作为参数传递。在这种情况下,假设您有两个 'IDocumentService' 的实现,一个称为 'DocumentService'(实际的),另一个称为 'TestDocumentService'(单元测试的)。那么 Unity Application Block(使用一种称为 Unity Pattern 的模式)允许您动态配置正确的实现来解决‘DmsController’与 'IDocumentService' 的依赖关系。

如果这仍然不清楚,让我展示一下我使用这个应用程序块在代码中注入依赖项的方式。要使用 Unity Application Block,您需要定义一个自定义控制器工厂。在上面的屏幕截图中,您可以看到一个名为*'UnityControllerFactory'* 的类。我将该类用作我的自定义控制器工厂。如果您查看该类的源代码,您会发现它继承自 MVC 的*'DefaultControllerFactory'*。这意味着我可以使用该类来替换默认的 MVC Controller Factory。根据 Unity Application Block 的要求,这个自定义类用于设置承载用于解析接口的详细信息的容器。让我向您展示*'Global.asax'* 的*'Application_Start'* 方法的样子。

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    RegisterRoutes(RouteTable.Routes);
    
    /*//Configure container with web.config
    UnityConfigurationSection section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
    section.Configure(_container, "containerOne");*/

    _container = new UnityContainer()
        .RegisterType<IDocumentService, DocumentService>(new ContainerControlledLifetimeManager())
        .RegisterType<IFileService, FileService>()
        .RegisterType<IDocumentRepository, DocumentRepository>()
        .RegisterType<IFileRepository, FileRepository>()
        .RegisterType<IRepositoryContext, DmsRepositoryContext>()
        .RegisterType<IFormsAuthenticationService, FormsAuthenticationService>()
        .RegisterType<IMembershipService, AccountMembershipService>();

    //Set for Controller Factory
    IControllerFactory controllerFactory = new UnityControllerFactory(_container);

    ControllerBuilder.Current.SetControllerFactory(controllerFactory);
}

如上所述,*'UnityContainer'* 用于将接口与其对应的具体实现注册。这只需要注册一次,因此建议将其放在*'Global.asax'* 的*'Application_Start'* 方法中。容器被传递给我的自定义 UnityControllerFactory。最后,您需要使用*'ControllerBuilder'* 的内置方法将我的控制器工厂设置为此应用程序的当前控制器工厂。其余都是自动的。有关此主题的更多详细信息,请访问 Unity Dependency Injection IoC Screencast ..>>

您还可以看到我的‘UnityControllerFactory’的样子…

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Microsoft.Practices.Unity;
using System.Web.Routing;
using System.ComponentModel;

namespace MvcDemo.Infrastracture.Mvc
{
    public class UnityControllerFactory : DefaultControllerFactory
    {
        private readonly IUnityContainer _container;

        public UnityControllerFactory(IUnityContainer container)
        {
            _container = container;
        }

        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            if (controllerType != null)
            {
                return _container.Resolve(controllerType) as IController;
            }
            return base.GetControllerInstance(requestContext, controllerType);
        }
    }
}

‘DefaultControllerFactory’代表默认注册的控制器工厂。此类提供了一个方便的基类,供那些只想对控制器创建进行微小更改的开发人员使用。如您所见,我们通过扩展默认控制器工厂创建了名为‘UnityControllerFactory’的自定义控制器工厂。

参考..

验证业务对象

用于存储数据的业务对象。业务功能的顺利运行取决于其相关业务对象的有效性。因此,在使用业务对象进行业务功能之前验证它们至关重要。例如,让我们以一个名为*'User'* 的业务对象为例,它有一个名为*'Password'* 的属性。我们还假设其长度必须至少为 8 个字符。那么,在存储密码之前,您需要进行长度验证,以确保该属性符合其业务要求。在 .NET 中,有多种对象验证选项。

  • 企业库的验证应用程序块
  • 工作流基金会、规则引擎
  • .NET 框架内置的*'Data Annotation Validator'*
  • ...等等。

验证可以在系统的不同层进行。一些验证可以在 UI(用户界面)层高效地完成。UI 层是用户输入验证最快、最有效的层。但是,由于各种限制,有些验证无法在 UI 层完成。例如,业务逻辑层是验证信用卡详细信息的最佳场所。

我们将在代码中使用*'Data Annotation Validator'*。它允许在 UI 层和 BLL 中进行验证。此外,如上所述,它是 .NET 框架的一部分。请参考一些在线资料查找有关*'Data Annotation Validator'* 的更多信息。快速搜索为我带来了这个

设计中的可伸缩性选项

由于我们为*'BLL'* 和*'DAL'* 分开了项目,所以我们也可以将这些层分解成物理层。例如,您可以将*'MvcDemo.Core'*(BLL)项目包装成一个‘WebService’,将其部署到单独的应用程序服务器上。您可以使用‘WebService’代理来与*'BLL'* 通信,同时将*'MvcDemo'* 网站保留在原始服务器上。同样的技巧也可以用来进一步扩展*'DAL'* 项目的系统。

ASP_NET_MVC_WITH_EF/ScalledSystem.jpg

*'MvcDemo.Common'* 项目是一个特殊的项目,它 agrup 了所有项目通用的公共操作。例如,我们在通用项目中进行异常日志记录、数据类型转换、空值处理、加密等。您可以在您公司的所有项目中重用此项目。所有您想要重用的通用函数都可以添加到此项目中。随着时间的推移,这个通用项目会越来越大,拥有越来越多的类来代表越来越多的可共享业务功能。

除了创建这个通用项目,您还可以扩展相同的概念来开发领域中心平台。领域中心平台将包含特定于某个领域的常用业务功能。为了构建这样的平台,您需要选择您的公司正在提供解决方案的领域。例如,如果您的公司专注于提供金融解决方案,那么您可以创建一个金融应用程序平台。这可以通过在每个金融项目完成后选择并添加业务功能到您的平台来实现。这种方法将逐渐构建一个平台,您可以将其作为基础框架来开发任何金融应用程序。

DMS 设计总结

ASP_NET_MVC_WITH_EF/ArchiSummary.jpg

正如您在上图中所看到的,系统有三个主要部分。

  • MVC - 包含视图和控制器。在我的设计中,我有一个名为*'DmsController'* 的控制器。该控制器应该控制所有 DMS 相关的操作。但是,如果我的系统为每个业务实体都有单独的控制器会更好。在这种情况下,因为我有两个业务实体,我应该有两个控制器,即*'DocumentController'* 和*'FileController'*。一个控制器可以关联一个或多个模型。例如,*'DocumentController'* 可以关联*'DocumentViewModel'*、*'DocumentEditModel'*、*'DocumentDetailViewModel'*、*'DocumentSummaryViewModel'* 等。

    有视图模型和编辑模型。视图模型用于在视图中显示详细信息。视图模型的属性需要直接映射到您在相关视图中显示的属性。编辑模型不同,用于捕获用户输入。例如,当您上传新文档或编辑现有文档的属性时,您需要使用*'DocumentEditModel'*。它必须与*'DocumentEditView'* 相关联。此外,编辑模型需要针对每个属性定义其各自的验证。
  • BLL - 业务逻辑层主要包含模型和服务。服务与映射(在我看来,我使用了*'AutoMapper'*,但还有许多其他功能丰富的库)库相关联,用于将数据对象映射到业务模型,反之亦然。
  • DAL - DAL 主要包含存储库、存储库上下文以及 Entity Framework 实体模型。

单元测试

单元测试是编程中非常重要的部分。它可以被视为对代码进行的第一次测试。它有助于开发人员及早发现错误。缺陷发现得越早,修复起来就越容易。

在此项目中,我们将使用*'Unity Application Block'* 与*'MsTest'* 一起执行单元测试。*'Unity Application Block'* 允许我们开发高度松耦合的 ASP.NET MVC 应用程序。我还没有完成源代码中的单元测试部分,但我将向您解释如何做到这一点。

看下图。这是我上面使用的同一个图。您会看到我完全移除了 DAL 部分,并将*'DocumentService'* 更改为*'TestDocService'*。我所做的是试图在控制*'DocumentService'* 的同时测试*'DmsController'* 的功能。为了做到这一点,我通过实现*'IDocumentService'* 接口开发了一个新的*'TestDocService'* 类。现在,我可以在*'TestDocService'* 中硬编码一切,而不是使用*'DocumentService'* 与 DAL 的依赖关系。换句话说,我可以用*'TestDocSercvice'* 创建一个自给自足的服务实现。您可以使用几个这样的测试实现来测试其他各种场景。例如,您可以实现*'DocumentService'* 的*'Test'* 版本来测试与“分页”相关的行为。它可以有一个*'GetDocumentList'* 方法实现,返回一个*'DocumentListViewModel'*,其中包含几百个*'DocumentViewModel'*。此测试实现可用于测试用户界面的分页部分。

ASP_NET_MVC_WITH_EF/TestingCodeInject.jpg

我发现这篇很棒的博客文章,您可以用它来进一步阅读此主题。

其他注意事项

异常处理是软件开发中最重要的方面之一。它提高了软件的质量。然而,开发人员有许多处理异常的技术。我认为将异常处理知识整合到一套完善的最佳实践和模式中很重要。在我的代码中,我决定在“Service”类中捕获/记录异常。您可以记录异常并将其包装在自定义异常中,然后将其抛回 UI 层。这样 UI 层就必须再次捕获异常并将用户重定向到相应的错误视图。然而,我决定去掉那部分,而是决定在出现异常时返回 null。坦率地说,这限制了您向用户显示正确错误消息的选项。您可以在*'DocumentService'* 类的实现中看到我是如何处理我代码中的异常的。

ASP_NET_MVC_WITH_EF/ExceptionHandling.jpg

进一步阅读...

结论

一个有效的设计需要分别且独特地解决每个特定问题,而不是将所有问题视为具有相同的需求。在实践中,这需要您运用经验。我提供了这个设计,希望以此为基础,您能发展出自己在这方面的概念。以后您可能需要重新审视这篇博文,也许是为了回顾和挑战您自己的概念。

历史

  • 2010/09/06 - 初始发布
© . All rights reserved.