在《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命名空間下),在數據庫初始化時不使用任何數據庫初始化策略,以此實現已存在數據庫的使用。這也使得讀者朋友能夠更為方便地部署和運行本案例程序。
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處理后,銷毀實例)。我們可以從以下代碼片段大致了解到這一點:
/// <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表達式。而實際上,這樣的改動是基於以下幾點考慮:
- 規約的具體實現的類名明確地表示了規約的動機,這樣有利於將規約作為通用語言的一個元素而參與到面向領域的討論中
- 面向對象的規約實現有助於模式應用,可以進一步考察實現倉儲動態查詢的可行性
基於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層產生的異常信息,大致有以下幾點需要注意:
- 在ByteartRetail.Services項目的AssemblyInfo.cs文件中,指定log4net的配置源:
[assembly: log4net.Config.XmlConfigurator(Watch = true)]
- 在ByteartRetail.Services項目的Global.asax.cs文件中,初始化log4net框架:
protected 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)以及系統集成和防腐層等相關專題。

