在《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)的演示案例具有以下改进:
在以下部分中会对上述内容作一些简单的介绍。
请【单击此处】下载Byteart Retail案例V2的源代码
注1:在上一个版本(V1)中,由于使用了不正确的数据库初始化策略,导致读者朋友在创建完数据库之后出现Entity Framework报错的问题(Migration数据库不存在的错误)。在V2中,ByteartRetailDbContext在初始化数据库时,将不再使用任何初始化策略,这就解决了V1中的上述问题
注2:在用户界面和功能上,V2和V1没有区别
根据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 ); } } } |
此改进来源于在同一个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表达式。而实际上,这样的改动是基于以下几点考虑:
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>
V2结合Unity的AOP拦截,使用log4net记录由Application层产生的异常信息,大致有以下几点需要注意:
[assembly: log4net.Config.XmlConfigurator(Watch = true)]
protected void Application_Start(object sender, EventArgs e) { ByteartRetailDbContextInitailizer.Initialize(); ApplicationService.Initialize(); log4net.Config.XmlConfigurator.Configure(); }
下图是在ByteartRetail.Services\Logs目录下产生的日志信息:
本文简要介绍了基于Entity Framework Code First的领域驱动设计案例:Byteart Retail的V2版本的一些改动和新特性,读者朋友可以使用文中提供的链接下载V2的源代码,如有疑问和建议,欢迎留言回复。在下一个版本的Byteart Retail中,我将继续研究领域事件的派发、Enterprise Service Bus(ESB)以及系统集成和防腐层等相关专题。
EntityFramework之领域驱动设计实践【Byteart Retail V2】
原文:http://blog.csdn.net/zhixiang2010/article/details/18946741