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

使用 S#arp Lite 开发设计良好的 ASP.NET MVC 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (7投票s)

2011年11月17日

CPOL

28分钟阅读

viewsIcon

41830

S#arp Lite 是一个使用 NHibernate 进行数据访问的、用于开发设计良好、定制化 ASP.NET MVC 应用程序的架构框架。

什么是 S#arp Lite?

S#arp Lite 是一个使用 NHibernate 进行数据访问的、用于开发设计良好、定制化 ASP.NET MVC 应用程序的架构框架。

ASP.NET MVC 3 是一个非常棒的 Web 应用程序开发平台。但是,和 ASP.NET 一样,它并没有为如何在不同的项目环境中最佳地使用它提供具体的指导。这当然是它的本意;它旨在提供一个灵活的平台,可以在各种场景下使用,而不会偏向于某种特定的架构,除了模型-视图-控制器(MVC)的基本原则。这样做的好处是你可以几乎以任何你喜欢的方式构建 MVC 项目;缺点是你的组织中的项目之间可能几乎没有一致性,即使它们都在使用 ASP.NET MVC。

这时,S#arp Lite 就派上用场了。S#arp Lite 提供了三个主要资产,为开发设计良好、符合 MVC 模式的应用程序提供了一个现成的解决方案:

  • 一个项目模板,用于简化新 S#arp Lite 项目的创建,并预先配置为使用 NHibernate 与数据库通信;
  • 一组可重用的类库,封装了基础设施的关注点(包括一个基础存储库);以及
  • 关于如何构建 S#arp Lite 项目的架构指导。

除了本文,架构指导还通过 S#arp Lite 发行包中包含的示例项目得到了体现。架构指导也通过项目层之间的依赖方向来强制执行。(稍后将对此进行更详细的讨论。)

总体的目标是让你的开发团队能够更轻松地开发遵循良好原则的 ASP.NET MVC 应用程序,例如 领域驱动设计测试驱动开发;而无需被基础设施的设置所困扰,并且不牺牲解决方案的长期可维护性和可扩展性。

顺便提一句,S#arp Lite 提供的基础存储库类有意地设计得非常简单。基础存储库只包含以下方法:

  • Get(id):从数据库中检索具有指定 ID 的实体,
  • GetAll():返回一个 IQueryable<>,可以通过 LINQ 进一步过滤/转换;
  • SaveOrUpdate(entity):将实体持久化到数据库,以及
  • Delete(entity):从数据库中删除一个实体。

保持基础存储库非常精简可以大大减少冗余,并将更多重点放在使用 LINQ 从 GetAll() 中检索结果上。此外,你可以用自己的代码扩展它,以增加更多功能。稍后我们将对此进行更详细的讨论。

这是为谁准备的?

S#arp Lite 的动机来自于与许多(包括我自己的)开发 S#arp Architecture 项目的团队合作。对许多人来说,S#arp Architecture 过于庞大的架构框架,让人难以完全理解。我过去在与考虑使用 S#arp Architecture 的团队讨论时,总是建议他们的开发人员必须非常有经验,并且精通诸如依赖注入、底层 NHibernate 和领域驱动设计等主题。

S#arp Lite 的动机来自于与许多(包括我自己的)开发 S#arp Architecture 项目的团队合作。对许多人来说,S#arp Architecture 过于庞大的架构框架,让人难以完全理解,并且与 NHibernate 耦合过紧。我过去在与考虑使用 S#arp Architecture 的团队讨论时,总是建议他们的开发人员必须非常有经验,并且精通诸如依赖注入、底层 NHibernate 和领域驱动设计等主题。

现实情况是,你的团队不太可能全是经验丰富的开发人员,而且他们都精通这些主题。S#arp Lite 旨在体现 S#arp Architecture 的核心价值,努力实现与大型项目同等的扩展性,同时又能被更广泛的受众所接受。换句话说,你应该能够拥有一个技能均衡的团队,并且仍然能够成功地交付 S#arp Lite 应用程序。

S#arp Lite 推荐用于任何中大型 ASP.NET MVC 项目。如果你是一个小型独立商店,你可能更适合使用一个层次较少的应用程序设置。S#arp Lite 可以很好地扩展到非常大的项目;我们目前正在将其有效地用于与六个其他系统集成的应用程序...所以它绝对能应对更复杂的挑战。

S#arp Lite 项目是什么样的?

创建一个新的 S#arp Lite 项目非常简单:

  • 从 GitHub 下载并解压 S#arp Lite 发行包
  • 遵循 README.txt 中的说明,使用 Templify(一个非常棒的小工具)来创建你的 S#arp Lite 项目。

创建 S#arp Lite 项目后,你会在根文件夹下找到以下目录结构:

  • app:此文件夹包含项目的源代码;即,你正在编写以获取报酬的代码。
  • build:这个最初为空的文件夹是为你的构建相关工件(如“publish”文件夹、NAnt 或 MSBuild 工件等)保留的占位符。
  • docs:这个最初为空的文件夹包含项目的所有文档。将其保存在此处可以使所有文档与代码一起被检入。
  • lib:此文件夹包含项目所有的 DLL 依赖项,例如 log4net.dll、SharpLite.Domain.dll、System.Web.Mvc.dll 等。
  • logs:这个最初为空的文件夹用于存放生成的日志文件。生成的项目 web.config 使用此文件夹来转储 log4net 日志。
  • tools:这个最初为空的文件夹用于存放团队在项目上工作可能需要的任何第三方安装文件或其他依赖项。例如,我们在此处存储 Telerik ASP.NET MVC 和 NUnit 的最新安装版本。将所有可安装的依赖项与代码一起检入,可以更容易地让“新来的家伙”快速上手。

自动生成的文件夹结构只是为了帮助保持数字资产和解决方案文件的组织性。更有趣的内容在 /app 文件夹中,它包含了你的项目源代码。但在深入了解 S#arp Lite 项目的 /app 文件夹中的每个项目层之前,让我们先对整体架构进行一个鸟瞰。

SharpLiteArchitecture.png

上图反映了 S#arp Lite 项目的层级,这些层级被实现为独立的类库和一个 ASP.NET MVC Web 项目。将这些层级放在独立的类库中,可以让你强制执行它们之间的依赖方向。例如,由于 YourProject.Tasks 依赖于 YourProject.Domain,YourProject.Domain 不能直接依赖于 YourProject.Tasks 中的任何类。这种单向依赖有助于强制保持架构的组织性。

虽然上图描述了每个层级的基本目的,但通过查看示例项目来更清晰地理解每个层级的职责范围是最有帮助的。因此,让我们来考察一下 S#arp Lite 发行包中包含的 MyStore 示例应用程序的层级。

考察 MyStore 示例应用程序的层级

发行版 Zip 中包含的 MyStore 示例应用程序演示了 S#arp Lite 在一个相当典型的 CRUD(创建/读取/更新/删除)应用程序中的使用。它包括管理数据和关系,如一对一、一对多、多对多和父子关系。让我们仔细看看关系模型。

上图代表了在 SQL Server 数据库中实现的关联对象模型。它是一个具有基本关系但非常简单的模型,但当我们将其转换为应用程序的面向对象设计时,它仍然会引发许多有趣的讨论点。

例如,每个客户实体包含地址信息(在 Customers 表中存储为 StreetAddress 和 ZipCode);在领域中,我们希望将地址信息提取到一个名为 Address 的单独对象中。将此信息作为一个单独的对象,可以更方便地为 Address 对象添加行为,同时将这些关注点与 Customer 对象分开,例如与 USPS 地址验证服务集成。(可以认为,这样的服务应该在一个独立的类中,但你懂我意思。)

作为另一个例子,多对多的 Products_ProductCategories 表不应该在我们的对象模型中有一个同名的类;相反,我们期望 Products 拥有一个 ProductCategories 的列表,反之亦然。

示例应用程序包含了关于如何通过 约定优于配置 实现所有这些以及类本身映射的示例。现在,让我们逐层查看示例应用程序,并讨论沿途有趣的要点。

MyStore.Domain

应用程序的这个层级包含应用程序的核心;它代表了我们产品的核心领域。所有其他层级仅用于支持用户与领域交互的需求。在 S#arp Lite 项目中,领域层包含四种类型的对象:

  • 领域对象
  • 查询对象
  • 查询接口
  • 自定义验证器

让我们逐一回顾。

领域对象

好的,所以这可能是一大堆对象类型,但它们都有相同的目的——它们用于实现我们应用程序的领域。它们包括实体、值对象、服务(例如计算器)、工厂、聚合根……所有这些都组织在模块中,通常表示为单独的命名空间。(我强烈推荐 Eric Evans 的 《领域驱动设计》,Jimmy Nilsson 的 《应用领域驱动设计和模式》,以及 Robert Martin 的 《软件开发:原则、模式和实践》 来帮助指导你。)示例项目中包含两个命名空间:“ProductMgmt”命名空间包含所有与产品管理相关的内容,而“root”命名空间则包含,嗯,其他所有内容。你的项目可能还会有其他命名空间。

现在让我们以示例项目中的 Customer 类为例,说明实体。有几点需要注意:

• Customer 继承自 Entity(来自 SharpLite.Domain.dll)。Entity 基类 A) 表明该类是具有数据库关联表的持久化对象,B) 提供了一个 Id 属性(这没什么特别的),C) 促进了两个实体之间的比较。如果两个实体类型相同且 ID 相同,那么你就知道它们是同一个对象。但是,如果你要比较两个“瞬态”实体呢?也就是说,两个尚未持久化到数据库的实体。作为另一个例子,你将如何比较一个瞬态实体与已持久化的实体?

为此,我们需要比较“领域签名”。领域签名是使实体在业务角度上独一无二的指纹。换句话说,一个对象的哪些属性使其可识别,即使没有 Id 属性?查看 Customer 类,我们发现有两个属性被“DomainSignature”属性装饰。此外,类本身被“HasUniqueDomainSignature”属性装饰。这意味着不能存在两个具有相同名字和姓氏的对象。(这在所有情况下都不适用;但应反映对象在应用程序上下文中的领域签名。)所描述的属性包含在 SharpLite.Domain.dll 中,并支持对类的领域签名进行自动验证。因此,如果你尝试添加一个与现有客户名字和姓氏都相同的客户,验证消息将告诉你这是不允许的。

[HasUniqueDomainSignature(...
public class Customer : Entity 
{ 
    [DomainSignature]
    ...
    public virtual string FirstName { get; set; }

• Customer 类将 Customers 表中的地址信息封装到一个单独的 Address 类中。有趣的是,NHibernate 的“loquacious”映射(也称为约定优于代码)会自动将相关的表列映射到这个“组件”对象中。

• Customer 拥有一个 IList of Orders,并且具有受保护的 setter;集合在构造函数中初始化。这有两个原因:1)按照设计,集合永远不会为 null(这避免了许多对象引用异常),2)我们不必担心 NHibernate 会丢失对从数据库加载的原始集合的引用。这种集合“模式”只是公开/保护集合的良好实践。

public class Customer : Entity
{
    public Customer() {
         Orders = new List();
    }

    public virtual IList Orders { get; protected set; }

• 使用标准的 .NET 数据注解强制执行验证。当所有内容都可以通过 .NET 库获得时,就不需要 NHibernate.Validator 或其他验证机制了。而且,当你遇到限制时,你可以创建自定义验证器。

查询对象

通常,我们需要过滤从数据库返回的信息。例如,我们可能只想返回“活跃”客户,而不是数据库中的所有客户。出于性能原因,显然最好将尽可能多的过滤工作交给数据库来完成。在 LINQ 提供程序出现之前,很难在领域端或数据库端找到适当的过滤平衡点。但是,有了 LINQ 和 IQueryable,可以在领域中开发过滤,而它仍然在数据库上执行。太棒了!为了实现这一点,每个存储库(例如 IRepository)都暴露了 GetAll() 方法,该方法返回 IQueryable<>。

这有一个惊人的附带好处,就是我们可以避免使用专门的“存储库”方法,这些方法仅仅是为了隐藏底层数据访问机制的细节。S#arp Lite 项目中有两种查询对象:

规范查询对象:规范查询对象接收一个列表,并根据某些标准将结果过滤到一个更小的列表中。在示例项目中,规范查询类 MyStore.Domain.FindActiveCustomers 提供了一个扩展方法给 IQueryable<>,并将任何过滤参数传递给该方法。或者,查询对象可以是一个 POCO 类,接受一个 IQueryable<> 并相应地进行过滤。将规范查询对象设置为扩展的优点是,多个查询可以链式调用,同时利用 IQuerable<> 的延迟查询(即,即使链接多个查询,也只会发送一个查询到数据库)。

public static class FindActiveCustomersExtension
{
    public static IQueryable FindActiveCustomers(this IQueryable customers) {
        return customers.FindActiveCustomers(MINIMUM_ORDERS_TO_BE_CONSIDERED_ACTIVE);
    }

    public static IQueryable FindActiveCustomers(this IQueryable customers, int minimumOrders) {
        return customers.Where(c =>
            c.Orders.Count >= minimumOrders);
    }

    private const int MINIMUM_ORDERS_TO_BE_CONSIDERED_ACTIVE = 3;
}

对我来说,真正美妙之处在于查询对象可以存在于领域中,并作为领域的一等公民进行测试,而不会引入对底层数据访问层的任何依赖(无论是 NHibernate、Entity Framework 等)。

报表查询对象:报表查询对象接收一个列表,在必要时进行过滤,然后将结果转换为 DTO 或 DTO 列表。想象一下你的应用程序中有一个摘要仪表板;例如,一个页面显示每个客户下了多少订单,以及每个客户最常购买的产品是什么。在这种情况下,我们最终希望得到一个 DTO 列表,其中包含每个客户的姓名、他/她的订单数量,以及他/她最喜欢的的产品。有几种方法可以解决这个问题:

  • 创建一个数据库存储过程,将结果绑定到 DTO 列表(从而将处理逻辑放在数据库上),
  • 使用 NHibernate Criteria、HQL 或命名查询来检索结果(并将数据访问代码与 NHibernate 紧密耦合),
  • 遍历领域端上的对象模型来汇总信息(你说是 n+1 吗?),或者
  • 使用干净、简单的、与数据访问无关的 LINQ(并将其保留在领域中)。

如果你猜到我倾向于哪种,就得 2 分。让我们以 MyStore.Domain.Queries.QueryForCustomerOrderSummariesExtension 为例。

public static class QueryForCustomerOrderSummariesExtension
{
    public static IQueryable QueryForCustomerOrderSummaries(this IQueryable customers) {
        return from customer in customers
            select new CustomerOrderSummaryDto() {
                FirstName = customer.FirstName,
                LastName = customer.LastName,
                OrderCount = customer.Orders.Count
            };
    }
}

同样,这样做的好处是它可以存在于领域层中,并作为可重用的报表查询,而不会引入对底层数据访问层的依赖。

你对如何使用查询对象以及它们的位置有很大的灵活性。例如,你可以在任务层的一个方法中使用即席报表查询(即,一个未封装在类中的 LINQ 查询)。虽然我不建议这样做,但你甚至可以在控制器的方法中使用即席查询。因此,提供的示例仅仅是示例,代表了一种特定的方法。最重要的是团队就如何封装和组织查询对象达成一致。在示例项目中,查询被封装为查询对象,并存储在“Queries”文件夹中——每个命名空间一个文件夹。

查询接口

在*非常*不可能的情况下,你需要直接利用数据访问机制,而不是 LINQ IQueryable,领域层还可以包含任何查询接口,这些接口定义了需要由数据访问层实现的查询。(这类似于在 S#arp Architecture 中创建自定义存储库接口。)

这种方法的缺点有三点:

  • 它在开发者之间引入了一层间接性;
  • 它将你的代码更紧密地耦合到底层数据访问层(因为你应用程序中的一些逻辑现在位于数据访问层);以及
  • 单元测试查询变得更加困难,因为你需要一个内存数据库或实时数据库来测试实现。

但是,我可以预见在某些情况下这是有必要的,如果你有一个非常复杂的查询,需要利用 NHibernate 的分离查询,或者仅仅是无法通过 LINQ 完成你需要做的事情。因此,需要采取三个步骤来支持该查询:

  • 在 YourAppProject.Domain 中定义查询接口(例如,MyStore.Domain.ProductMgmt.Queries.IQueryForProductOrderSummaries.cs);
  • 在 YourAppProject.NHibernateProvider 中实现具体的查询类(例如,MyStore.NHibernateProvider.Queries.QueryForProductOrderSummaries.cs);以及
  • 使用 IoC 注册实现,以解析对接口的请求(例如,MyStore.Init.DependencyResolverInitializer)。

显然,这不像使用规范查询对象和报表查询对象那样简洁,但如果绝对必要,也是可行的。

自定义验证器

如前所述,S#arp Lite 使用 .NET 的数据注解来支持验证。(如果你愿意,可以使用其他方式,例如 NHibernate.Validator。)数据注解直接添加到实体类中,但也可以添加到表单 DTO 中,如果你认为实体不应同时充当表单验证对象。

有时,数据注解的强大功能不足以满足你的领域需求;例如,如果你想比较两个属性。因此,你可以开发 自定义验证器 并将它们存储在“Validators”文件夹中。如果自定义验证器是类特定的,并且从不重用,那么我通常会将自定义验证器类添加为使用它的类的私有子类。这样,类特定的验证器就可以整齐地隐藏起来,只有需要它的类才能访问。S#arp Lite 使用自定义验证器来确定一个对象是否是现有对象的重复,利用其领域签名:\SharpLiteSrc\app\SharpLite.Domain\Validators\HasUniqueDomainSignatureAttribute.cs。

MyStore.Tasks

应用程序的这个层级包含任务协调逻辑,响应来自表示层(例如控制器)的命令。(马丁·福勒(Martin Fowler)在其 《企业应用模式》 中也将此层级描述为 服务层。)例如,假设你的应用程序集成了许多其他应用程序。此层级将与所有其他应用程序(最好通过接口)通信,整理信息,然后将其交给领域层以在数据上执行领域逻辑。更简单的例子是,如果你的领域层包含某种 FinancialCalculator 类,任务层将从存储库或其他来源收集计算器所需的信息,并通过方法将数据传递给 FinancialCalculator。

作为次要职责,任务层将数据(作为视图模型、DTO 或实体)返回给表示层。例如,表示层可能需要显示客户列表,以及如果登录用户拥有足够的权限,则显示创建/编辑/删除按钮。任务层将获取要显示的客户列表,并确定用户的安全访问权限;然后它将返回一个视图模型,其中包含客户列表以及一个描述用户是否具有修改数据权限的布尔值(或安全对象)。

重要的是要注意任务层中的逻辑与领域层中的逻辑之间的区别。任务层应包含最少的逻辑来协调服务(例如存储库、Web 服务等)和领域层(例如计算器服务)之间的活动。将任务层视为一个有效的领导者(存在这种事情吗?)……领导者有助于促进团队成员之间的沟通,并告诉团队成员做什么,但自己不做工作。

任务层包含两种类型的对象:

  • 任务对象
  • 视图模型

任务对象

这些是任务本身。最常见的任务是协调 CUD(创建、更新、删除)逻辑(CRUD 中的 CUD)。它非常普遍,以至于 S#arp Lite 项目包含一个(完全可定制的)BaseEntityCudTasks 类来封装这一常见需求。查看示例项目,你可以看到 BaseEntityCudTasks 如何被扩展和使用;例如,在 MyStore.Tasks.ProductMgmt.ProductCudTasks 中。

随着项目的增长,任务层职责不可避免地也会增长。例如,在一个当前集成了多个外部应用程序的项目中,一个任务类从 Primavera 6 拉取计划信息,从 Prism 拉取成本信息,并通过存储库从数据库获取本地数据。然后,它将所有这些信息传递给位于领域中的 MasterReportGenerator 类。因此,尽管任务类并非微不足道,但它仅仅是从各种来源提取数据,并将繁重的数据处理留给领域。

重要的是要注意,任务对象的服务依赖项(存储库、Web 服务、查询接口等)应通过 依赖注入 进行注入。这使得能够使用 存根/模拟 服务来单元测试任务对象。使用 MVC 3,设置依赖注入非常简单,并且可以轻松定义任务对象的依赖项。

public ProductCategoryCudTasks(IRepository<ProductCategory> productCategoryRepository) 
    : base(productCategoryRepository) {

    _productCategoryRepository = productCategoryRepository;
}

在这里,我们看到 ProductCategoryCudTasks 类需要在其构造函数中注入一个 IRepository,该实例将在运行时由 IoC 容器提供,或者在你进行单元测试时由你提供。

视图模型

视图模型类封装了要显示给用户的信息。它不说明数据*如何*显示,只说明*哪些*数据应该显示。通常,它还会包含支持信息,供表示层决定*如何*显示信息;例如,权限信息。

关于视图模型类应该放在哪里,有很多争论。在我的项目中,我将它们放在任务层,每个命名空间一个 ViewModels 文件夹。但可以说,视图模型类可以放在一个单独的类库中;这是你的团队在项目开始时需要决定的。

MyStore.Web

这里没什么可说的。S#arp Lite 项目在表示层使用了所有现成的 MVC 3 功能,默认使用 Razor 视图引擎,如果你愿意,可以进行更改。此层中唯一的 S#arp Lite 特征(这绝对是一个词)如下:

  • MyStore.Web.Global.asax 调用 DependencyResolverInitializer.Initialize(); 来初始化 IoC 容器(稍后讨论);
  • MyStore.Web.Global.asax 使用 SharpModelBinder 作为首选的表单/模型绑定器;以及
  • Web.config 包含一个 HttpModule,用于利用 S#arp Lite 源代码中 \SharpLiteSrc\app\SharpLite.NHibernateProvider\Web\SessionPerRequestModule.cs 提供的 每个请求一个会话的 NHibernate HTTP 模块

SharpModelBinder 扩展了基本的表单/模型绑定功能,使其能够填充关联。例如,假设你有一个 Product 类,它与 ProductCategory 之间存在多对多关系。在编辑 Product 时,视图可以包含一个复选框列表,用于将产品与一个或多个产品类别相关联。SharpModelBinder 在表单中查找此类关联,并在发布到控制器时填充关联;即,发布到控制器的 Product 将包含其 ProductCategories,其中包含一个 ProductCategory,对应于每个被选中的复选框。你可以查看 MyStore.Web/Areas/ProductMgmt/Views/Products/Edit.cshtml 作为示例。

与任务对象一样,控制器也通过注入接受依赖项;例如 MyStore.Web.Areas.ProductMgmt.Controllers.ProductsController.cs。

public ProductsController(IRepository<Product> productRepository,
    ProductCudTasks productMgmtTasks, 
    IQueryForProductOrderSummaries queryForProductOrderSummaries) {

    _productRepository = productRepository;
    _productMgmtTasks = productMgmtTasks;
    _queryForProductOrderSummaries = queryForProductOrderSummaries;
}

在上面的示例中,控制器在其构造函数中需要一个 IRepository、ProductCudTasks 和 IQueryForProductOrderSummaries 的实例,这些实例由 IoC 容器提供。IQueryForProductOrderSummaries 是使用领域中定义的查询接口来满足数据访问层特定需求的示例。这是一个非常特殊的案例,仅包含用于说明目的。你几乎总是可以使用规范查询对象和报表查询对象...或者直接使用 LINQ 查询 IRepository.GetAll()。

如果你想了解更多关于 ASP.NET MVC 3 中的依赖注入的信息,请查看 Brad Wilson 关于该主题的 系列博文。要了解 Web 层开发的基础知识,Steve Sanderson 的 《Pro ASP.NET MVC 3 Framework》 是一个很好的读物。

MyStore.Init

这个几乎是精简的层只有一个职责:执行通用的应用程序初始化逻辑。具体来说,S#arp Lite 项目中包含的初始化代码会初始化 IoC 容器(StructureMap)并调用 NHibernate 会话工厂的初始化。可以说,这一层非常薄,其职责很容易被 MyStore.Web 所涵盖。将初始化代码提取到一个单独的类库中的巨大优势是 MyStore.Web 需要更少的依赖项。请注意,MyStore 对 NHibernate.dll 或 StructureMap.dll 没有任何引用。因此,从 Web 层到这些依赖项的耦合非常少(即,没有)...我们喜欢这样。除其他外,这可以防止任何人从控制器调用特定于 NHibernate 的函数。反过来,这也使控制器与底层数据访问机制保持高度解耦。

MyStore.NHibernateProvider

S#arp Lite 项目层级之旅的下一站是 NHibernate 提供程序层。在 S#arp Architecture 中,这一层通常会包含大量的自定义存储库和命名查询。通过使用查询对象和 IQueryable<> 上的 LINQ 作为替代方案,这个类库应该保持非常精简。这个类库包含三种类型的对象:

  • NHibernate 初始化器;
  • NHibernate 约定;
  • 映射覆盖;以及
  • (非常偶尔) 查询实现。

让我们逐一看看。

NHibernate 初始化器

NHibernate 3.2.0 引入了一个内置的流畅 API 用于配置和映射类,绰号为 NHibernate 的“Loquacious API”。这是对 Fluent NHibernator 的直接 挑战(我曾经非常喜欢它……詹姆斯·格雷戈里,致以诚挚的感谢),我觉得随着这些功能现在直接内置到 NHibernate 中,它注定要过时了。NHibernate 3.2 的 Loquacious API 还没有 Fluent NHibernate 强大,但随着 ConfORM 的更多内容被移植到 Loquacious API,它很快就会达到相同的水平。继续演示……

S#arp Lite 项目中有一个 NHibernate 初始化类;例如,MyStore.NHibernateProvider.NHibernateInitializer.cs。这个类设置连接字符串(来自 web.config),设置方言,告诉 NHibernate 在哪里查找映射的类,并调用约定设置(稍后讨论)。初始化 NHibernate 非常耗时,应该只在应用程序启动时执行一次。因此,如果你决定将 IoC 初始化代码(在 MyStore.Init 中)替换为另一个 IoC 容器,请注意这一点。

NHibernate 约定

约定的好处在于,我们不再需要包含一个类映射(HBM 或其他)来将类映射到数据库。我们只需定义约定,遵循这些约定,NHibernate 就知道去哪个表/列查找什么。S#arp Lite 项目预装了以下*可定制*的约定:

  • 表名是实体名称的复数形式。例如,如果实体是 Customer,表就是 Customers。
  • 每个实体都有一个 Id 属性,映射到一个“Id”标识列(可以轻松更改为 HiLo、Guid 或其他)。
  • 基本类型列名与属性名相同。例如,如果属性是 FirstName,列名就是 FirstName。
  • 外键(关联)的名称是属性名加上“Fk”后缀。例如,如果属性是 Order.WhoPlacedOrder,列名(在 Orders 表中)是 WhoPlacedOrderFk,并具有到相应类型的表的(例如 Customers)外键。

S#arp Lite 项目通常只有一个约定设置类;例如 MyStore.NHibernateProvider.Conventions。唯一不支持“开箱即用”的约定是多对多关系,我们稍后将对此进行更多讨论。

映射覆盖

有时约定会失效。例如:

  • 多对多关系;
  • 枚举作为属性类型;
  • 遗留数据库不遵循(你的)约定;以及
  • 任何时候由于某种原因未遵循约定。

好的一面是,这种情况并不多……但我们需要能够处理例外情况。任何偏离约定的情况都被定义为“映射覆盖”。覆盖的示例可以在 MyStore.NHibernateProvider/Overrides 中找到。为了方便起见,如果需要添加覆盖,只需实现 MyStore.NHibernateProvider.Overrides.IOverride,并包含你的覆盖代码。MyStore.NHibernateProvider.Conventions 类会遍历程序集查找任何实现了 IOverride 的类,并依次应用它们。作为一项规则,我为每个需要覆盖的实体创建一个覆盖类。

查询实现

最后,.NHibernateProvider 层包含任何 NHibernate 特定的查询,这些查询是 .Domain 层中相应查询接口的实现。97.6831% 的时间,这并不是必需的,因为通过 IQueryable<> 上的 LINQ 进行查询是首选的查询方法。但在极少数情况下,你需要使用 NHibernate Criteria、HQL 或其他方式实现查询,这里就是它的归宿。示例已包含在 MyStore.NHibernateProvider.Queries.QueryForProductOrderSummaries 中。

MyStore.Tests

在我们对 S#arp Lite 项目层级进行考察的最后,是 .Tests 层。这一层包含项目的所有单元测试。S#arp Lite 项目默认生成两个单元测试:

  • MyStore.Tests.NHibernateProvider.MappingIntegrationTests.CanGenerateDatabaseSchema():这个单元测试初始化 NHibernate 并生成 SQL 来反映这些映射。这是一个很好的测试,可以用来验证一个类是否按照预期映射到数据库;也就是说,你可以查看生成的 SQL(在 NUnit 的 Text Output 选项卡中)来验证类是如何被映射的。作为一项附带的好处,你可以复制生成的 SQL 并运行它来修改数据库。最后,单元测试将生成的 SQL 保存到 /app/MyStore.DB/Schema/UnitTestGeneratedSchema.sql 中以供进一步参考。
  • MyStore.Tests.NHibernateProvider.MappingIntegrationTests.CanConfirmDatabaseMatchesMappings():这个单元测试初始化 NHibernate 并验证每个实体是否都能成功映射到数据库。如果你有一个缺失的列,这个测试会告诉你。它不会测试所有内容,例如多对多关系,但肯定会测试其他 97.6831% 的内容。

要了解测试驱动开发的入门知识,请阅读 Kent Beck 的 《测试驱动开发:示例》。更进一步,可以阅读 Gerard Meszaros 的 《xUnit 测试模式》 和 Michael Feathers 的 《有效处理遗留代码》

在 S#arp Architecture 中,使用 SQLLite 来提供一个内存数据库来测试自定义存储库方法。由于自定义存储库现在大多被淘汰了,将 SQLLite 测试内置到 S#arp Lite 中会显得多余,并且已被移除以保持简单。(此外,如果需要,你随时可以参考 S#arp Architecture 代码来获取该功能。)

S#arp Lite 库中有什么?

在通过示例项目介绍 S#arp Lite 类库时,大部分相关内容已经讨论过了,但让我们花点时间看看可重用的 S#arp Lite 类库中都包含哪些内容。

SharpLite.Domain

这个类库为你的 S#arp Lite 项目的领域层提供了支持。

  • ComparableObject.cs:为任何继承它的对象提供健壮的哈希码生成器。(实现起来比你想象的要复杂得多。)
  • DomainSignatureAttribute.cs:一个用于装饰构成类领域签名的属性的属性。
  • EntityWithTypedId(在 Entity.cs 中定义):为你的实体提供一个(非强制性的)泛型基类,包括一个 Id 属性以及在比较相似对象时包含领域签名属性。它接受一个泛型参数,声明 Id 属性的类型;例如,int 或 Guid。
  • Entity.cs:提供一个 EntityWithTypedId 基类,其中 Id 假定为 int 类型。
  • IEntityWithTypedId.cs:提供一个接口,可用于实现自己的实体基类。
  • /DataInterfaces/IDbContext.cs:公开一个用于控制事务的接口。
  • /DataInterfaces/IEntityDuplicateChecker.cs:公开一个用于检查实体是否是数据库中已存在实体的重复项的接口。
  • /DataInterfaces/IRepository.cs:公开一个非常基础的存储库,包括 Get、GetAll(返回 IQueryable)、SaveorUpdate 和 Delete。
  • /Validators/HasUniqueDomainSignatureAttribute.cs:一个可用于装饰类的属性,以确保数据库中不存在具有相同领域签名的重复项。

SharpLite.EntityFrameworkProvider

这个库的目的是提供一个可插入的替代 NHibernateProvider(稍后讨论),如果团队选择这样做的话。这个库还没有完全开发。但如果你有兴趣贡献,请告诉我!

SharpLite.NHibernateProvider

这个基础设施类库提供了 S#arp Lite 项目通过 NHibernate 与数据库通信所需的一切。

  • DbContext.cs:实现 IDbContext 以进行事务管理。
  • EntityDuplicateChecker.cs:实现 IEntityDuplicateChecker 以检查实体重复项。
  • LazySessionContext.cs:支持 NHibernate 的 open-session-in-view;最初由 Jose Romaniello 编写
  • Repository.cs:提供一个极简的基础存储库……可以根据需要进行扩展。
  • /Web/SessionPerRequestModule.cs:提供支持 NHibernate open-session-in-view 的 HTTP 模块。

SharpLite.Web

这个类库为 S#arp Lite 项目提供 MVC 特定的需求……这完全由 SharpModelBinder.cs 组成。对于你在 S#arp Lite 项目中的使用,这完全是可选的。

  • /Mvc/ModelBinder/EntityCollectionValueBinder.cs:由 SharpModelBinder 用于将表单中的输入集合转换为绑定到包含对象的实体集合。
  • /Mvc/ModelBinder/EntityRetriever.cs:由 SharpModelBinder 相关类用于从数据库检索实体,而无需事先知道要使用哪个存储库。
  • /Mvc/ModelBinder/EntityValueBinder.cs:由 SharpModelBinder 用于将下拉列表选择转换为绑定到包含对象的实体关联。
  • /Mvc/ModelBinder/SharpModelBinder.cs:扩展 ASP.NET MVC 模型绑定器,增加了填充数据库实体关联的功能。


好了,暂时就这么多。我真诚地希望 S#arp Lite 能在开发设计良好、可维护且能随着项目发展而良好扩展的 ASP.NET MVC 应用程序方面对你和你的团队有所帮助。这个架构框架反映了多年来的经验教训:经验血汗泪以及无数厚颜无耻地从那些比我聪明得多的人那里偷来的想法。

尽情享用!
Billy McCafferty
http://devlicio.us/blogs/billy_mccafferty

© . All rights reserved.