EntityFramework之领域驱动设计实践【后续篇】:基于EF 4.3.1 Code First的领域驱动设计实践案例
两年前我在博客中发布了《EntityFramework之领域驱动设计实践》系列文章,也得到了广大读者朋友的关注,在完成了系列文章的总结之后,也一直没有这部分内容的更新了。现在,Entity Framework的稳定版(就是那个Stable的版本,不是Entity Framework 5的beta版本)4.3.1已经逐步应用到各种.NET项目中,为了演示Entity Framework 4.3.1 Code First编程模式以及其它的一些.NET技术在领域驱动设计实践上的应用,我重新采用经典的分层架构(也就是类似Microsoft NLayerApp的区别于CQRS的架构)实现了一个案例程序:Byteart Retail。在这个案例中,仓储的实现不再采用NHibernate,而是使用的Entity Framework 4.3.1 Code First。虽然这个案例较为完整地从各方面展示了.NET技术在企业级程序中的应用,但Entity Framework确实是其亮点之一,因此,我将这篇介绍这个案例的文章也编排在原来的EF系列文章中。
案例概述
Byteart Retail以笔记本电脑在线销售业务为背景,展示了Microsoft.NET技术以及领域驱动设计理念在软件设计、架构与实践中的应用。在Byteart Retail之前,开源社区也有一些领域驱动设计的实践案例,比如Microsoft Domain-Oriented NLayered Application Architecture、面向CQRS体系结构模式的Tiny Library CQRS等。与这些案例相比,Byteart Retail在领域驱动的实践指导方面有一定的相似性和可比性,但它更注重Microsoft.NET技术与领域驱动设计相结合。比如,Byteart Retail案例对基于Entity Framework 4.3.1版本中Code First模式的仓储实现进行了全方位的演示,这样的Entity Framework仓储设计,使得领域模型对象能够完全设计为POCO对象而不需要依赖任何其它技术框架,因此,在对仓储的选择和使用上,就能够做到“无缝替换”。另一方面,领域模型的设计也更为考究,实体、值对象的设计、聚合的划分等,都与系统业务紧密结合,相对于其它的演示案例更为成熟。从实现的业务逻辑上看,Byteart Retail大致实现了以下功能:
- 笔记本电脑商品的浏览
- 客户账户注册和基本信息查询与修改
- 笔记本电脑详细信息查询
- 客户添加笔记本电脑商品到购物篮
- 购物篮商品项目管理
- 从购物篮创建销售订单
- 销售订单的确认与查询
- 销售订单明细查询
案例对以下.NET技术和开发技巧进行了演示:
- Microsoft Entity Framework 4.3.1 Code First
- ASP.NET MVC 3
- WCF
- Microsoft Patterns & Practices Unity Application Block
- 使用AutoMapper实现DTO与领域对象映射
- T4自动化代码生成
案例下载
请【单击此处】下载本案例的所有源代码和Visual Studio 2010的解决方案文件。
系统需求
开发环境:Visual Studio 2010 Professional/Ultimate with SP1,ASP.NET MVC3。其它的程序集引用都在压缩包的packages目录下,因此读者无需上网下载安装其它组件。
安装部署
数据库
Byteart Retail采用Microsoft SQL Server 2008作为后台数据库。首先,修改ByteartRetail.Services项目下的web.config文件,对数据库链接字符串进行配置:
然后,按照下面“程序启动”部分的描述,启动ByteartRetail.Web项目,此时Entity Framework会根据上面的连接字符串创建一个名为ByteartRetail的数据库。
最后,打开SQL Server Management Studio,执行压缩包中SQL目录下的ByteartRetailData.sql文件即可将所需的测试数据导入ByteartRetail数据库中。
说明:这种数据库的部署和初始化方式虽然能够规避“Model compatibility cannot be checked because the database does not contain model metadata.”的错误,但仍然不是一个很理想的部署方式。在这里我们暂时采用这种方式让案例先运行起来,以后我会找出一个更合理的办法并对这部分内容进行更新。
程序启动
使用Visual Studio打开解决方案并完成编译,然后在ByteartRetail.Services项目中任选一个.svc文件点击右键,并选择View in Browser选项以启动WCF Service;之后,直接运行ByteartRetail.Web项目,即可出现主界面。
设计概要
在此我先将部分设计的类图贴出,以方便读者朋友在查看源代码的过程中参阅。
领域模型
基于Entity Framework的仓储设计(省略属性与方法)
规约设计
企业应用架构模式参考
本案例大致涉及到了以下企业级应用架构模式,也一并列举于此,供读者朋友们参考,也可以作为学习《Patterns of Enterprise Application Architecture》、《Core J2EE Patterns》等书籍的参考。
- Domain Model
- Unit Of Work
- Repository
- Data Transfer Object
- Client Session State
- Layer Supertype
- Separated Interface
- Value Object
- Business Delegate
- Service Locator
- Transfer Object Assembler
总结
热烈欢迎爱好Microsoft.NET技术以及领域驱动设计的读者朋友对本案例进行深入讨论。有疑问或建议请直接留言回复。
在《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的源代码
部署运行
- 解开Byteart Retail V2的压缩包
- 在SQL Server数据库中,新建一个名为ByteartRetail的数据库
- 运行SQL目录下的ByteartRetail.sql数据库脚本,这将创建与本案例相关的数据表
- 在Visual Studio 2010中打开Byteart Retail.sln解决方案,打开ByteartRetail.Services项目下的Web.config文件
- 根据自己的数据库配置情况,更改Entity Framework所使用的数据库连接字符串,注意启用MARS选项
- 在ByteartRetail.Services项目下,找到任意一个.svc文件,单击鼠标右键并选择View In Browser菜单,这将启动ASP.NET Development Server,并在浏览器中打开选中的WCF服务页面
- 启动ByteartRetail.Web项目以显示用户界面
注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项目中找到:
1
2
3
4
5
|
<!--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方法给出了答案:
1
2
3
4
5
6
7
|
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表达式。而实际上,这样的改动是基于以下几点考虑:
- 规约的具体实现的类名明确地表示了规约的动机,这样有利于将规约作为通用语言的一个元素而参与到面向领域的讨论中
- 面向对象的规约实现有助于模式应用,可以进一步考察实现仓储动态查询的可行性
基于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层的接口类型指定拦截器类型以及拦截行为:
1
2
3
4
5
6
|
<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层产生的异常信息,大致有以下几点需要注意:
- 在ByteartRetail.Services项目的AssemblyInfo.cs文件中,指定log4net的配置源:
1[assembly: log4net.Config.XmlConfigurator(Watch =
true
)]<br>
- 在ByteartRetail.Services项目的Global.asax.cs文件中,初始化log4net框架:
123456protected
void
Application_Start(
object
sender, EventArgs e)
{
ByteartRetailDbContextInitailizer.Initialize();
ApplicationService.Initialize();
log4net.Config.XmlConfigurator.Configure();
}
- 在ByteartRetail.Services项目的Web.config中,配置log4net。详情请见此文件
下图是在ByteartRetail.Services\Logs目录下产生的日志信息:
总结
本文简要介绍了基于Entity Framework Code First的领域驱动设计案例:Byteart Retail的V2版本的一些改动和新特性,读者朋友可以使用文中提供的链接下载V2的源代码,如有疑问和建议,欢迎留言回复。在下一个版本的Byteart Retail中,我将继续研究领域事件的派发、Enterprise Service Bus(ESB)以及系统集成和防腐层等相关专题。
03 2012 档案
posted @ 2012-03-23 10:39 dax.net 阅读(2075) | 评论 (17) 编辑 |
posted @ 2012-03-16 15:47 dax.net 阅读(4931) | 评论 (22) 编辑 |
posted @ 2012-03-01 11:03 dax.net 阅读(1096) | 评论 (2) 编辑 |