EntityFramework之领域驱动设计实践【Byteart Retail V2】

在《EntityFramework之领域驱动设计实践【后续篇】:基于EF 4.3.1 Code First的领域驱动设计实践案例》一文中,我给出了一个基于Entity Framework 4.3.1 Code First的领域驱动设计实践案例:Byteart Retail。此案例得到了广大读者朋友的关注,也有很多网友针对案例中的各种实现技术进行提问,我也基本上一一回答了大家的疑问。为了能够更好地演示领域驱动设计在基于Microsoft .NET技术上的实践,我对Byteart Retail作了进一步完善,现将改进版的Byteart Retail案例(简称Byteart Retail V2)发布于此,供大家参阅。

与上一个版本的Byteart Retail案例相比,新版本(V2)的演示案例具有以下改进:

  • 中文注释(不断完善中)
  • 已存在数据库的使用
  • 基于Unity的WCF Per-Request Lifetime Manager
  • 面向特定需求的仓储接口
  • 规约的具体实现
  • 基于Unity的AOP拦截
  • 使用log4net记录拦截的Exception详细信息

在以下部分中会对上述内容作一些简单的介绍。

Byteart Retail V2案例源代码下载

【单击此处】下载Byteart Retail案例V2的源代码

部署运行

  1. 解开Byteart Retail V2的压缩包
  2. 在SQL Server数据库中,新建一个名为ByteartRetail的数据库
  3. 运行SQL目录下的ByteartRetail.sql数据库脚本,这将创建与本案例相关的数据表
  4. 在Visual Studio 2010中打开Byteart Retail.sln解决方案,打开ByteartRetail.Services项目下的Web.config文件
  5. 根据自己的数据库配置情况,更改Entity Framework所使用的数据库连接字符串,注意启用MARS选项
    EntityFramework之领域驱动设计实践【Byteart Retail V2】
  6. 在ByteartRetail.Services项目下,找到任意一个.svc文件,单击鼠标右键并选择View In Browser菜单,这将启动ASP.NET Development Server,并在浏览器中打开选中的WCF服务页面
    EntityFramework之领域驱动设计实践【Byteart Retail V2】
  7. 启动ByteartRetail.Web项目以显示用户界面
    EntityFramework之领域驱动设计实践【Byteart Retail V2】

注1:在上一个版本(V1)中,由于使用了不正确的数据库初始化策略,导致读者朋友在创建完数据库之后出现Entity Framework报错的问题(Migration数据库不存在的错误)。在V2中,ByteartRetailDbContext在初始化数据库时,将不再使用任何初始化策略,这就解决了V1中的上述问题

注2:在用户界面和功能上,V2和V1没有区别

 

V2的功能改进

中文注释(不断完善中)

根据V1一文中网友的反馈意见,从V2开始我将慢慢地使用中文注释代替原来的英文注释,但整个项目的源代码文件比较多,我平时的个人时间也有限,因此没法一次性全部更新完,只能是在今后的版本升级中不断完善。当然,我也会在版本升级的过程中抽空逐步完善当前版本中的注释内容,并更新文章中的下载链接,所以只能希望网友们:请多关照+敬请谅解+欢迎关注。

已存在数据库的使用

V2中更新了ByteartRetailDbContextInitializer类型的Initialize公共静态方法(该方法位于ByteartRetail.Domain.Repository项目、ByteartRetail.Domain.Repositories.EntityFramework命名空间下),在数据库初始化时不使用任何数据库初始化策略,以此实现已存在数据库的使用。这也使得读者朋友能够更为方便地部署和运行本案例程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
namespace ByteartRetail.Domain.Repositories.EntityFramework
{
    /// <summary>
    /// 表示由Byteart Retail专用的数据访问上下文初始化器。
    /// </summary>
    public sealed class ByteartRetailDbContextInitailizer : DropCreateDatabaseIfModelChanges<ByteartRetailDbContext>
    {
        // 请在使用ByteartRetailDbContextInitializer作为数据库初始化器(Database Initializer)时,去除以下代码行
        // 的注释,以便在数据库重建时,相应的SQL脚本会被执行。对于已有数据库的情况,请直接注释掉以下代码行。
        //protected override void Seed(ByteartRetailDbContext context)
        //{
        //    context.Database.ExecuteSqlCommand("CREATE UNIQUE INDEX IDX_CUSTOMER_USERNAME ON Customers(UserName)");
        //    context.Database.ExecuteSqlCommand("CREATE UNIQUE INDEX IDX_CUSTOMER_EMAIL ON Customers(Email)");
        //    context.Database.ExecuteSqlCommand("CREATE UNIQUE INDEX IDX_LAPTOP_NAME ON Laptops(Name)");
        //    base.Seed(context);
        //}
 
        /// <summary>
        /// 执行对数据库的初始化操作。
        /// </summary>
        public static void Initialize()
        {
            Database.SetInitializer<ByteartRetailDbContext>(null);
        }
    }
}

基于Unity的WCF Per-Request Lifetime Manager

此改进来源于在同一个Request中保证RepositoryContext的一致性问题。在一个WCF操作上下文中,很多情况下Application层的任务协调会涉及到多个Repository,而这些Repository都应该共享同一个RepositoryContext,以便所有的操作能通过RepositoryContext进行一次提交,完成Unit Of Work。在V1的案例中,Application层中每一个需要用到Repository的地方,都会使用RepositoryContextManager来确保RepositoryContext实例的一致性,而后又会使用RepositoryContextManager.GetRepository方法返回针对特定聚合根的仓储实例。这样做虽然确保了RepositoryContext实例的一致性,但同时也失去了Repository的扩展性:我们只能使用EntityFrameworkRepository泛型类型的Repository实现,而其提供的仓储方法又极为有限。

因此,V2采用基于Unity的WCF Per-Request Lifetime Manager来解决这样的矛盾。由于WCF服务层是通过Unity IoC容器来获得Application层的具体实现(表现为ServiceLocator模式的应用),因此在Application层就能够获得由Unity通过构造器注入的RepositoryContext以及Repository的实例,并且此时的RepositoryContext的生命周期是由WCF Per-Request Lifetime Manager托管的(每次WCF Request发起时,Resolve一个新的实例,完成WCF Request处理后,销毁实例)。我们可以从以下代码片段大致了解到这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/// <summary>
/// 表示与“客户”相关的应用层服务的一种实现。
/// </summary>
public class CustomerServiceImpl : ApplicationService, ICustomerService
{
    #region Private Fields
    private readonly ICustomerRepository customerRepository;
    private readonly IShoppingCartRepository shoppingCartRepository;
    private readonly ISalesOrderRepository salesOrderRepository;
    #endregion
 
    #region Ctor
    /// <summary>
    /// 初始化一个<c>CustomerServiceImpl</c>类型的实例。
    /// </summary>
    /// <param name="context">用来初始化<c>CustomerServiceImpl</c>类型的仓储上下文实例。</param>
    /// <param name="customerRepository">“客户”仓储实例。</param>
    /// <param name="shoppingCartRepository">“购物车”仓储实例。</param>
    /// <param name="salesOrderRepository">“销售订单”仓储实例。</param>
    public CustomerServiceImpl(IRepositoryContext context,
        ICustomerRepository customerRepository,
        IShoppingCartRepository shoppingCartRepository,
        ISalesOrderRepository salesOrderRepository)
        :base(context)
    {
        this.customerRepository = customerRepository;
        this.shoppingCartRepository = shoppingCartRepository;
        this.salesOrderRepository = salesOrderRepository;
    }
    #endregion
 
    #region ICustomerService Members
    /// <summary>
    /// 根据给定的客户信息,创建客户对象。
    /// </summary>
    /// <param name="dataObject">包含了客户信息的数据传输对象。</param>
    /// <returns>已创建客户对象的全局唯一标识。</returns>
    public Guid CreateCustomer(CustomerDataObject dataObject)
    {
        if (dataObject == null)
            throw new ArgumentNullException("customerDataObject");
 
        if (customerRepository.UserNameExists(dataObject.UserName))
            throw new DomainException("Customer with the UserName of ‘{0}‘ already exists.", dataObject.UserName);
 
        if (customerRepository.EmailExists(dataObject.Email))
            throw new DomainException("Customer with the Email of ‘{0}‘ already exists.", dataObject.Email);
 
        Customer customer = Mapper.Map<CustomerDataObject, Customer>(dataObject);
        ShoppingCart shoppingCart = customer.CreateShoppingCart();
 
        customerRepository.Add(customer);
        shoppingCartRepository.Add(shoppingCart);
 
        Context.Commit();
 
        return customer.ID;
    }
     
    // ****其它代码部分忽略****
    #endregion
}

 

而在ByteartRetail.Services项目的Web.config中,配置IRepositoryContext的Lifetime Manager为WcfPerRequestLifetimeManager。WcfPerRequestLifetimeManager的具体实现代码可以在ByteartRetail.Infrastructure项目中找到:

<!--Repository Context & Repositories-->
<register type="ByteartRetail.Domain.Repositories.IRepositoryContext, ByteartRetail.Domain"
      mapTo="ByteartRetail.Domain.Repositories.EntityFramework.EntityFrameworkRepositoryContext, ByteartRetail.Domain.Repositories">
  <lifetime type="ByteartRetail.Infrastructure.WcfPerRequestLifetimeManager, ByteartRetail.Infrastructure"/>
</register>

 

面向特定需求的仓储接口

由于V2解耦了RepositoryContextManager与Repository的具体实现,因此我们可以很方便地自定义面向特定需求的仓储接口。在ByteartRetail.Domain项目的Repositories子目录下,新增了类似IXXXRepository(比如:ICustomerRepository、ISalesOrderRepository等)这样的仓储接口,而这些接口又实现了IRepository泛型接口。

ByteartRetail.Domain.Repositories项目下包含了对这些IXXXRepsitory接口的实现类,这些类不仅实现了IXXXRepository接口,而且继承于EntityFrameworkRepository泛型类,以便能够直接使用那些已定义的标准仓储操作。在介绍V1一文的评论部分,有朋友提出,如果需要按多个实体属性进行排序,标准的仓储接口应该如何操作。在V2中,LaptopRepository的GetAllLaptops方法给出了答案:

public IEnumerable<Laptop> GetAllLaptops()
{
    var query = EFContext.Context.Set<Laptop>()
        .OrderBy(l => l.UnitPrice)
        .ThenBy(l => l.Name);
    return query.ToList();
}

 

这种实现方式的另一个好处是,当今后我发现需要用其它的字段进行排序时,我可以重新实现ILaptopRepository接口,并在实现类中处理排序问题,而不需要去修改LaptopRepository类甚至是ILaptopRepository接口以使其提供其它字段的排序功能。

规约的具体实现

在V1的源代码中,所有传递给Repository的规约都是通过Specification泛型类的Eval方法,通过传入Lambda表达式而产生的。在V2中,这些代码都被规约的具体实现所取代:我们可以在ByteartRetail.Domain.Repositories项目的Specifications目录下找到这些实现类。

从表面上看,使用Eval会更方便编程,而且规约的具体实现本质上也是Lambda表达式。而实际上,这样的改动是基于以下几点考虑:

  1. 规约的具体实现的类名明确地表示了规约的动机,这样有利于将规约作为通用语言的一个元素而参与到面向领域的讨论中
  2. 面向对象的规约实现有助于模式应用,可以进一步考察实现仓储动态查询的可行性

基于Unity的AOP拦截

V2使用了Unity的一个扩展(Extension)来实现AOP拦截。该扩展名为Unity Interception Extension,可以在NuGet Package Manager中找到。需要使用Unity拦截功能的项目,不仅要添加对Unity的引用,而且还需要添加对Unity Interception Extension的引用。

为了演示AOP拦截,V2定义了一个拦截行为:ExceptionLoggingBehavior,用于在Application层发生异常时,将异常信息写入日志文件。此拦截行为的源代码位于ByteartRetail.Infrastructure项目的InteceptionBehaviors目录下,在Invoke方法中使用Utils工具类处理捕获的异常。

在ByteartRetail.Services项目的Web.config文件里,当注册Unity容器时,我们需要针对Application层的接口类型指定拦截器类型以及拦截行为:

<register type="ByteartRetail.Application.ICustomerService, ByteartRetail.Application"
          mapTo="ByteartRetail.Application.Implementation.CustomerServiceImpl, ByteartRetail.Application">
  <interceptor type="InterfaceInterceptor"/>
  <interceptionBehavior 
    type="ByteartRetail.Infrastructure.InterceptionBehaviors.ExceptionLoggingBehavior, ByteartRetail.Infrastructure"/>
</register>

 

使用log4net记录拦截的Exception详细信息

V2结合Unity的AOP拦截,使用log4net记录由Application层产生的异常信息,大致有以下几点需要注意:

  1. 在ByteartRetail.Services项目的AssemblyInfo.cs文件中,指定log4net的配置源:
    [assembly: log4net.Config.XmlConfigurator(Watch = true)]
  2. 在ByteartRetail.Services项目的Global.asax.cs文件中,初始化log4net框架:
    protected void Application_Start(object sender, EventArgs e)
    {
        ByteartRetailDbContextInitailizer.Initialize();
        ApplicationService.Initialize();
        log4net.Config.XmlConfigurator.Configure();
    }

  3. 在ByteartRetail.Services项目的Web.config中,配置log4net。详情请见此文件

下图是在ByteartRetail.Services\Logs目录下产生的日志信息:

EntityFramework之领域驱动设计实践【Byteart Retail V2】

 

总结

本文简要介绍了基于Entity Framework Code First的领域驱动设计案例:Byteart Retail的V2版本的一些改动和新特性,读者朋友可以使用文中提供的链接下载V2的源代码,如有疑问和建议,欢迎留言回复。在下一个版本的Byteart Retail中,我将继续研究领域事件的派发、Enterprise Service Bus(ESB)以及系统集成和防腐层等相关专题。

EntityFramework之领域驱动设计实践【Byteart Retail V2】

上一篇:selenium (四) WebDriverWait 与 expected_conditions


下一篇:js基础-键盘事件