# [.NET領域驅動設計實戰系列]專題十:DDD擴展內容:全面剖析CQRS模式實現
## 一、引言
前面介紹的所有專題都是基于經典的領域驅動實現的,然而,領域驅動除了經典的實現外,還可以基于CQRS模式來進行實現。本專題將全面剖析如何基于CQRS模式(Command Query Responsibility Segregation,命令查詢職責分離)來實現領域驅動設計。
## 二、CQRS是什么?
在介紹具體的實現之前,對于之前不了解CQRS的朋友來說,首先第一個問題應該是:什么是CQRS啊?你倒是詳細介紹完CQRS后再介紹具體實現啊?既然大家會有這樣的問題,所以本專題首先全面介紹下什么是CQRS。
## 2.1 CQRS發展歷程
在介紹CQRS之前,我覺得有必要先了解一下CQS(即Command Query Separation,命令查詢分離)模式。我們可以理解CQRS是在DDD的實踐中基于CQS理論而出現的一種體系結構模式。CQS模式最早由軟件大師Bertrand Meyer(Eiffel語言之父,面向對象開-閉原則OCP提出者)提出,他認為,對象的行為僅有兩種:命令和查詢,不存在第三種情況。根據CQS的思想,任何方法都可以拆分為命令和查詢兩部分。例如下面的方法:
在上面的方法中,執行了一個命令,即對變量_number加上一個因子factor,同時又執行了一個查詢,即查詢返回_number的值。根據CQS的思想,該方法可以拆成Command和Query兩個方法:
```
private int _number = 0;
private void AddCommand(int factor)
{
_number += factor;
}
private int QueryValue()
{
return _number;
}
```
命令和查詢分離使得我們可以更好地把握對象的細節,更好地理解哪些操作會改變系統的狀態。從而使的系統具有更好的擴展性,并獲得更好的性能。
CQRS根據CQS思想,并結合領域驅動設計思想,由Grey Young在[CQRS, Task Based UIs, Event Sourcing agh!](http://codebetter.com/gregyoung/2010/02/16/cqrs-task-based-uis-event-sourcing-agh/) 這篇文章中提出。**CQRS將之前只需要定義一個對象拆分成兩個對象,分離的原則按照對象中方法是執行命令還是執行查詢來進行拆分的。**
## **2.2 CQRS結構**
由前面的介紹可知,采用CQRS模式實現的系統結構可以分為兩個部分:命令部分和查詢部分。其系統結構如下圖所示:

從上面系統結構圖可以發現,采用CQRS實現的領域驅動設計與經典DDD有很大的不同。采用CQRS實現的DDD結構大體分為兩部分,查詢部分和命令部分,并且維護著兩個數據庫實例,一個專門用來進行查詢,另一個用來響應命令操作。然后通過EventHandler操作將命令改變的狀態同步到用來查詢的數據庫實例中。從這個描述中,我們可能會聯想到數據庫級別主從讀寫分離。然而數據讀寫分離是在數據庫層面來實現讀寫分離的機制,而CQRS是在業務邏輯層面來實現讀寫分離機制。兩者是站在兩個不同的層面對讀寫分離進行實現的。
## 三、為什么需要引入CQRS模式
前面我們已經詳細介紹了CQRS模式,相信經過前面的介紹,大家對CQRS模式一定有一些了解了,但為什么要引入CQRS模式呢?
在傳統的實現中,對DB執行增、刪、改、查所有操作都會放在對應的倉儲中,并且這些操作都公用一份領域實體對象。對于一些簡單的系統,使用傳統的設計方式并沒有什么不妥,但在一些大型復雜的系統中,傳統的實現方式也會存在一些問題:
* 使用同一個領域實體來進行數據讀寫可能會遇到資源競爭的情況。所以經常要處理鎖的問題,在寫入數據的時候,需要加鎖,讀取數據的時候需要判斷是否允許臟讀。這樣使得系統的邏輯性和復雜性增加,并會影響系統的吞吐量。
* 在大數據量同時進行讀寫的情況下,可能出現性能的瓶頸。
* 使用同一個領域實體來進行數據庫讀寫可能會太粗糙。在大多是情況下,比如編輯操作,可能只需要更新個別字段,這時卻需要將整個對象都穿進去。還有在查詢的時候,表現層可能只需要個別字段,但需要查詢和返回整個領域實體,再把領域實體對象轉換從對應的DTO對象。
* 讀寫操作都耦合在一起,不利于對問題的跟蹤和分析,如果讀寫操作分離的話,如果是由于狀態改變的問題就只需要去分析寫操作相關的邏輯就可以了,如果是關于數據的不正確,則只需要關心查詢操作的相關邏輯即可。
針對上面的這些問題,采用CQRS模式的系統都可以解決。由于CQRS模式中將查詢和命令進行分析,所以使得兩者分工明確,各自負責不同的部分,并且在業務上將命令和查詢分離能夠提高系統的性能和可擴展性。既然CQRS這么好,那是不是所有系統都應該基于CQRS模式去實現呢?顯然不是的,CQRS也有其使用場景:
1. 系統的業務邏輯比較復雜的情況下。因為本來業務邏輯就比較復雜了,如果再把命令操作和查詢操作綁定同一個業務實體的話,這樣會導致后期的需求變更難于進行擴展下去。
2. 需要對系統中查詢性能和寫入性能分開進行優化的情況下,尤其讀/寫比例非常高的情況下。例如,在很多系統中讀操作的請求數遠大于寫操作,此時,就可以考慮將寫操作抽離出來進行單獨擴展。
3. 系統在將來隨著時間不斷變化的情況下。
然而,CQRS也有其不適用的場景:
* 業務邏輯比較簡單的情況下,此時采用CQRS反而會把系統搞的復雜。
* 系統用戶訪問量都比較小的情況下,并且需求以后不怎么會變更的情況下。針對這樣的系統,完全可以用傳統的實現方式快速將系統實現出來,沒必要引入CQRS來增加系統的復雜度。
## 四、事件溯源
**在CQRS中,查詢方面,直接通過方法查詢數據庫,然后通過DTO將數據返回,這個方面的操作相對比較簡單。而命令方面,是通過發送具體Command,接著由CommandBus來分發到具體的CommandHandle來進行處理,CommandHandle在進行處理時,并沒有直接將對象的狀態保存到外部持久化結構中,而僅僅是從領域對象中獲得產生的一系列領域事件,并將這些事件保存到Event Store中,同時將事件發布到事件總線Event Bus進行下一步處理;接著Event Bus同樣進行協調,將具體的事件交給具體的Event Handle進行處理,最后Event Handler再把對象的狀態保存到對應Query數據庫中。**
上面過程正是CQRS系統中的調用順序。從中可以發現,采用CQRS實現的系統存在兩個數據庫實例,一個是Event Store,該數據庫實例用來保存領域對象中發生的一系列的領域事件,簡單來說就是保存領域事件的數據庫。另一個是Query Database,該數據庫就是存儲具體的領域對象數據的,查詢操作可以直接對該數據庫進行查詢。由于,我們在Event Store中記錄領域對象發生的所有事件,這樣我們就可以通過查詢該數據庫實例來獲得領域對象之前的所有狀態了。所謂Event Sourcing,就是指的的是:通過事件追溯對象的起源,它允許通過記錄下來的事件,將領域模型恢復到之前的任意一個時間點。
通過Event來記錄領域對象所發生的所有狀態,這樣利用系統的跟蹤并能夠方便地回滾到某一歷史狀態。經過上面的描述,感覺事件溯源一般用于系統的維護。例如,我們可以設計一個同步服務,該服務程序從Event Store數據庫查詢出領域對象的歷史數據,從而打印生成一個歷史報表,如歷史價格報表等。但正是的CQRS系統中如何使用Event Sourcing的呢?
在前面介紹CQRS系統的調用順序中,我們講到,由Event Handler將對象的狀態保存到對應的Query數據庫中,這里有一個問題,對象的狀態怎么獲得呢?對象狀態的獲得正是由Event sourcing機制來獲得,因為用戶發送的僅僅是Command,Command中并不包含對象的狀態數據,所以此時需要通過Event Sourcing機制來查詢Event Store來還原對象的狀態,還原根據就是對應的Id,該Id是通過命令傳入的。**Event Sourcing的調用需要放在CommandHandle中,因為CommandHandle需要先獲得領域對象,這樣才能把領域對象與命令對象來進行對比,從而獲得領域對象中產生的一系列領域事件。**
## 五、快照
然而,當隨著時間的推移,領域事件變得越來越多時,通過Event Sourcing機制來還原對象狀態的過程會非常耗時,因為每一次都需要從最早發生的事件開始。那有沒有好的一個方式來解決這個問題呢?答案是肯定的,即在Event Sourcing中引入快照(Snapshots)實現。實現原理就是——沒產生N個領域事件,則對對象做一次快照。這樣,**領域對象溯源的時候,可以先從快照中獲得最近一次的快照,然后再逐個應用快照之后所有產生的領域事件,而不需要每次溯源都從最開始的事件開始對對象重建,這樣就大大加快了對象重建的過程。**
## 六、CQRS模式實現和剖析
前面介紹了那么多CQRS的內容,下面就具體通過一個例子來演示下CQRS系統的實現。
命令部分的實現
```
// 應用程序初始化操作,將依賴的對象通過依賴注入框架StructureMap進行注入
public sealed class ServiceLocator
{
private static readonly ICommandBus _commandBus;
private static readonly IStorage _queryStorage;
private static readonly bool IsInitialized;
private static readonly object LockThis = new object();
static ServiceLocator()
{
if (!IsInitialized)
{
lock (LockThis)
{
// 依賴注入
ContainerBootstrapper.BootstrapStructureMap();
_commandBus = ContainerBootstrapper.Container.GetInstance<ICommandBus>();
_queryStorage = ContainerBootstrapper.Container.GetInstance<IStorage>();
IsInitialized = true;
}
}
}
public static ICommandBus CommandBus
{
get { return _commandBus; }
}
public static IStorage QueryStorage
{
get { return _queryStorage; }
}
}
class ContainerBootstrapper
{
private static Container _container;
public static void BootstrapStructureMap()
{
_container = new Container(x =>
{
x.For(typeof (IDomainRepository<>)).Singleton().Use(typeof (DomainRepository<>));
x.For<IEventStorage>().Singleton().Use<InMemoryEventStorage>();
x.For<IEventBus>().Use<EventBus>();
x.For<ICommandBus>().Use<CommandBus>();
x.For<IStorage>().Use<InMemoryStorage>();
x.For<IEventHandlerFactory>().Use<StructureMapEventHandlerFactory>();
x.For<ICommandHandlerFactory>().Use<StructureMapCommandHandlerFactory>();
});
}
public static Container Container
{
get { return _container;}
}
}
public class HomeController : Controller
{
[HttpPost]
public ActionResult Add(DiaryItemDto item)
{
// 發布CreateItemCommand到CommandBus中
ServiceLocator.CommandBus.Send(new CreateItemCommand(Guid.NewGuid(), item.Title, item.Description, -1, item.From, item.To));
return RedirectToAction("Index");
}
}
// CommandBus 的實現
public class CommandBus : ICommandBus
{
private readonly ICommandHandlerFactory _commandHandlerFactory;
public CommandBus(ICommandHandlerFactory commandHandlerFactory)
{
_commandHandlerFactory = commandHandlerFactory;
}
public void Send<T>(T command) where T : Command
{
// 獲得對應的CommandHandle來對命令進行處理
var handlers = _commandHandlerFactory.GetHandlers<T>();
foreach (var handler in handlers)
{
// 處理命令
handler.Execute(command);
}
}
}
// 對CreateItemCommand處理類
public class CreateItemCommandHandler : ICommandHandler<CreateItemCommand>
{
private readonly IDomainRepository<DiaryItem> _domainRepository;
public CreateItemCommandHandler(IDomainRepository<DiaryItem> domainRepository)
{
_domainRepository = domainRepository;
}
// 具體處理邏輯
public void Execute(CreateItemCommand command)
{
if (command == null)
{
throw new ArgumentNullException("command");
}
if (_domainRepository == null)
{
throw new InvalidOperationException("domainRepository is not initialized.");
}
var aggregate = new DiaryItem(command.ID, command.Title, command.Description, command.From, command.To)
{
Version = -1
};
// 將對應的領域實體進行保存
_domainRepository.Save(aggregate, aggregate.Version);
}
}
// IDomainRepository的實現類
public class DomainRepository<T> : IDomainRepository<T> where T : AggregateRoot, new()
{
// 并沒有直接對領域實體進行保存,而是先保存領域事件進EventStore,然后在Publish事件到EventBus進行處理
// 然后EventBus把事件分配給對應的事件處理器進行處理,由事件處理器來把領域對象保存到QueryDatabase中
public void Save(AggregateRoot aggregate, int expectedVersion)
{
if (aggregate.GetUncommittedChanges().Any())
{
_storage.Save(aggregate);
}
}
}
// Event Store的實現,這里保存在內存中,通常是保存到具體的數據庫中,如SQL Server、Mongodb等
public class InMemoryEventStorage : IEventStorage
{
// 領域事件的保存
public void Save(AggregateRoot aggregate)
{
// 獲得對應領域實體未提交的事件
var uncommittedChanges = aggregate.GetUncommittedChanges();
var version = aggregate.Version;
foreach (var @event in uncommittedChanges)
{
version++;
// 沒3個事件創建一次快照
if (version > 2)
{
if (version % 3 == 0)
{
var originator = (ISnapshotOrignator)aggregate;
var snapshot = originator.CreateSnapshot();
snapshot.Version = version;
SaveSnapshot(snapshot);
}
}
@event.Version = version;
// 保存事件到EventStore中
_events.Add(@event);
}
// 保存事件完成之后,再將該事件發布到EventBus 做進一步處理
foreach (var @event in uncommittedChanges)
{
var desEvent = TypeConverter.ChangeTo(@event, @event.GetType());
_eventBus.Publish(desEvent);
}
}
}
// EventBus的實現
public class EventBus : IEventBus
{
private readonly IEventHandlerFactory _eventHandlerFactory;
public EventBus(IEventHandlerFactory eventHandlerFactory)
{
_eventHandlerFactory = eventHandlerFactory;
}
public void Publish<T>(T @event) where T : DomainEvent
{
// 獲得對應的EventHandle來處理事件
var handlers = _eventHandlerFactory.GetHandlers<T>();
foreach (var eventHandler in handlers)
{
// 對事件進行處理
eventHandler.Handle(@event);
}
}
}
// DiaryItemCreatedEvent的事件處理類
public class DiaryIteamCreatedEventHandler : IEventHandler<DiaryItemCreatedEvent>
{
private readonly IStorage _storage;
public DiaryIteamCreatedEventHandler(IStorage storage)
{
_storage = storage;
}
public void Handle(DiaryItemCreatedEvent @event)
{
var item = new DiaryItemDto()
{
Id = @event.SourceId,
Description = @event.Description,
From = @event.From,
Title = @event.Title,
To = @event.To,
Version = @event.Version
};
// 將領域對象持久化到QueryDatabase中
_storage.Add(item);
}
}
```
上面代碼主要演示了Command部分的實現,從代碼可以看出,首先我們需要通過ServiceLocator類來對依賴注入對象進行注入,然后UI層通過CommandBus把對應的命令發布到CommandBus中進行處理,命令總線再查找對應的CommandHandler來對命令進行處理,接著CommandHandler調用倉儲類來保存領域對象對應的事件,保存事件成功后再將事件發布到事件總線中進行處理,然后由對應的事件處理程序將領域對象保存到QueryDatabase中。這樣就完成了命令部分的操作,從中可以發現,命令部分的實現和CQRS系統中的系統結構圖的處理過程是一樣的。然而創建日志命令并沒有涉及事件溯源操作,因為創建命令并需要重建領域對象,此時的領域對象是通過創建日志命令來獲得的,但在修改和刪除命令中涉及了事件溯源,因為此時需要根據命令對象的ID來重建領域對象。具體的實現可以參考源碼。
下面讓我們再看看查詢部分的實現。
查詢部分的實現代碼:
```
public class HomeController : Controller
{
// 查詢部分
public ActionResult Index()
{
// 直接獲得QueryDatabase對象來查詢所有日志
var model = ServiceLocator.QueryStorage.GetItems();
return View(model);
}
}
public class InMemoryStorage : IStorage
{
private static readonly List<DiaryItemDto> Items = new List<DiaryItemDto>();
public DiaryItemDto GetById(Guid id)
{
return Items.FirstOrDefault(a => a.Id == id);
}
public void Add(DiaryItemDto item)
{
Items.Add(item);
}
public void Delete(Guid id)
{
Items.RemoveAll(i => i.Id == id);
}
public List<DiaryItemDto> GetItems()
{
return Items;
}
}
```
從上面代碼可以看出,查詢部分的代碼實現相對比較簡單,UI層直接通過QueryDatabase來查詢領域對象,然后由UI層進行渲染出來顯示。
到此,一個簡單的CQRS系統就完成了,然而在項目中,UI層并不會直接CommandBus和QueryDatabase進行引用,而是通過對應的CommandService和QueryService來進行協調,具體的系統結構如下圖所示(只是在CommandBus和Query Database前加入了一個SOA的服務層來進行協調,這樣有利于系統擴展,可以通過SOA服務來進行請求路由,將不同請求路由不同的系統中,這樣會可以實現多個系統進行一個整合):

關于該CQRS系統的演示效果,大家可以自行去Github或MSDN中進行下載,具體的下載地址將會本專題最后給出。
## 七、總結
到這里,本專題關于CQRS的介紹就結束了,并且本專題也是領域驅動設計系列的最后一篇了。本系列專題的內容主要是參考daxnet的ByteartRetail案例,由于daxnet在寫這個案例的時候并沒有一步一步介紹其創建過程,對于一些領域驅動的初學者來說,直接去學習這個案例未免會有點困難,導致學習興趣降低,從而放棄領域驅動的學習。為了解決這些問題,所以,本人對ByteartRetail案例進行剖析,并參考該案例一步步實現自己的領域驅動案例OnlineStore。希望本系列可以幫助大家打開領域驅動的大門。
由于現在NO-SQL在互聯網行業的應用已經非常流行,以至于面試的時候經常會被問到你用過的非關系數據庫有哪些?所以本人也不想Out,所以在最近2個月的時候學習了一些No-SQL的內容,所以,接下來,我將會開啟一個NO-SQL系列,記錄自己這段時間來學習NO-SQL的一些心得和體會。
本專題所有源碼下載:
Github地址:[https://github.com/lizhi5753186/CQRSDemo](https://github.com/lizhi5753186/CQRSDemo)
MSDN地址:[https://code.msdn.microsoft.com/CQRS-1f05ebe5](https://code.msdn.microsoft.com/CQRS-1f05ebe5)
本文參考鏈接:
[http://www.codeproject.com/Articles/555855/Introduction-to-CQRS](http://www.codeproject.com/Articles/555855/Introduction-to-CQRS%20)
[http://www.cnblogs.com/daxnet/archive/2010/08/02/1790299.html](http://www.cnblogs.com/daxnet/archive/2010/08/02/1790299.html)
- C# 基礎知識系列
- C# 基礎知識系列 專題一:深入解析委托——C#中為什么要引入委托
- C# 基礎知識系列 專題二:委托的本質論
- C# 基礎知識系列 專題三:如何用委托包裝多個方法——委托鏈
- C# 基礎知識系列 專題四:事件揭秘
- C# 基礎知識系列 專題五:當點擊按鈕時觸發Click事件背后發生的事情
- C# 基礎知識系列 專題六:泛型基礎篇——為什么引入泛型
- C# 基礎知識系列 專題七: 泛型深入理解(一)
- C# 基礎知識系列 專題八: 深入理解泛型(二)
- C# 基礎知識系列 專題九: 深入理解泛型可變性
- C#基礎知識系列 專題十:全面解析可空類型
- C# 基礎知識系列 專題十一:匿名方法解析
- C#基礎知識系列 專題十二:迭代器
- C#基礎知識 專題十三:全面解析對象集合初始化器、匿名類型和隱式類型
- C# 基礎知識系列 專題十四:深入理解Lambda表達式
- C# 基礎知識系列 專題十五:全面解析擴展方法
- C# 基礎知識系列 專題十六:Linq介紹
- C#基礎知識系列 專題十七:深入理解動態類型
- 你必須知道的異步編程 C# 5.0 新特性——Async和Await使異步編程更簡單
- 全面解析C#中參數傳遞
- C#基礎知識系列 全面解析C#中靜態與非靜態
- C# 基礎知識系列 C#中易混淆的知識點
- C#進階系列
- C#進階系列 專題一:深入解析深拷貝和淺拷貝
- C#進階系列 專題二:你知道Dictionary查找速度為什么快嗎?
- C# 開發技巧系列
- C# 開發技巧系列 使用C#操作Word和Excel程序
- C# 開發技巧系列 使用C#操作幻燈片
- C# 開發技巧系列 如何動態設置屏幕分辨率
- C# 開發技巧系列 C#如何實現圖片查看器
- C# 開發技巧 如何防止程序多次運行
- C# 開發技巧 實現屬于自己的截圖工具
- C# 開發技巧 如何使不符合要求的元素等于離它最近的一個元素
- C# 線程處理系列
- C# 線程處理系列 專題一:線程基礎
- C# 線程處理系列 專題二:線程池中的工作者線程
- C# 線程處理系列 專題三:線程池中的I/O線程
- C# 線程處理系列 專題四:線程同步
- C# 線程處理系列 專題五:線程同步——事件構造
- C# 線程處理系列 專題六:線程同步——信號量和互斥體
- C# 多線程處理系列專題七——對多線程的補充
- C#網絡編程系列
- C# 網絡編程系列 專題一:網絡協議簡介
- C# 網絡編程系列 專題二:HTTP協議詳解
- C# 網絡編程系列 專題三:自定義Web服務器
- C# 網絡編程系列 專題四:自定義Web瀏覽器
- C# 網絡編程系列 專題五:TCP編程
- C# 網絡編程系列 專題六:UDP編程
- C# 網絡編程系列 專題七:UDP編程補充——UDP廣播程序的實現
- C# 網絡編程系列 專題八:P2P編程
- C# 網絡編程系列 專題九:實現類似QQ的即時通信程序
- C# 網絡編程系列 專題十:實現簡單的郵件收發器
- C# 網絡編程系列 專題十一:實現一個基于FTP協議的程序——文件上傳下載器
- C# 網絡編程系列 專題十二:實現一個簡單的FTP服務器
- C# 互操作性入門系列
- C# 互操作性入門系列(一):C#中互操作性介紹
- C# 互操作性入門系列(二):使用平臺調用調用Win32 函數
- C# 互操作性入門系列(三):平臺調用中的數據封送處理
- C# 互操作性入門系列(四):在C# 中調用COM組件
- CLR
- 談談: String 和StringBuilder區別和選擇
- 談談:程序集加載和反射
- 利用反射獲得委托和事件以及創建委托實例和添加事件處理程序
- 談談:.Net中的序列化和反序列化
- C#設計模式
- UML類圖符號 各種關系說明以及舉例
- C#設計模式(1)——單例模式
- C#設計模式(2)——簡單工廠模式
- C#設計模式(3)——工廠方法模式
- C#設計模式(4)——抽象工廠模式
- C#設計模式(5)——建造者模式(Builder Pattern)
- C#設計模式(6)——原型模式(Prototype Pattern)
- C#設計模式(7)——適配器模式(Adapter Pattern)
- C#設計模式(8)——橋接模式(Bridge Pattern)
- C#設計模式(9)——裝飾者模式(Decorator Pattern)
- C#設計模式(10)——組合模式(Composite Pattern)
- C#設計模式(11)——外觀模式(Facade Pattern)
- C#設計模式(12)——享元模式(Flyweight Pattern)
- C#設計模式(13)——代理模式(Proxy Pattern)
- C#設計模式(14)——模板方法模式(Template Method)
- C#設計模式(15)——命令模式(Command Pattern)
- C#設計模式(16)——迭代器模式(Iterator Pattern)
- C#設計模式(17)——觀察者模式(Observer Pattern)
- C#設計模式(18)——中介者模式(Mediator Pattern)
- C#設計模式(19)——狀態者模式(State Pattern)
- C#設計模式(20)——策略者模式(Stragety Pattern)
- C#設計模式(21)——責任鏈模式
- C#設計模式(22)——訪問者模式(Vistor Pattern)
- C#設計模式(23)——備忘錄模式(Memento Pattern)
- C#設計模式總結
- WPF快速入門系列
- WPF快速入門系列(1)——WPF布局概覽
- WPF快速入門系列(2)——深入解析依賴屬性
- WPF快速入門系列(3)——深入解析WPF事件機制
- WPF快速入門系列(4)——深入解析WPF綁定
- WPF快速入門系列(5)——深入解析WPF命令
- WPF快速入門系列(6)——WPF資源和樣式
- WPF快速入門系列(7)——深入解析WPF模板
- WPF快速入門系列(8)——MVVM快速入門
- WPF快速入門系列(9)——WPF任務管理工具實現
- ASP.NET 開發
- ASP.NET 開發必備知識點(1):如何讓Asp.net網站運行在自定義的Web服務器上
- ASP.NET 開發必備知識點(2):那些年追過的ASP.NET權限管理
- ASP.NET中實現回調
- 跟我一起學WCF
- 跟我一起學WCF(1)——MSMQ消息隊列
- 跟我一起學WCF(2)——利用.NET Remoting技術開發分布式應用
- 跟我一起學WCF(3)——利用Web Services開發分布式應用
- 跟我一起學WCF(3)——利用Web Services開發分布式應用
- 跟我一起學WCF(4)——第一個WCF程序
- 跟我一起學WCF(5)——深入解析服務契約 上篇
- 跟我一起學WCF(6)——深入解析服務契約 下篇
- 跟我一起學WCF(7)——WCF數據契約與序列化詳解
- 跟我一起學WCF(8)——WCF中Session、實例管理詳解
- 跟我一起學WCF(9)——WCF回調操作的實現
- 跟我一起學WCF(10)——WCF中事務處理
- 跟我一起學WCF(11)——WCF中隊列服務詳解
- 跟我一起學WCF(12)——WCF中Rest服務入門
- 跟我一起學WCF(13)——WCF系列總結
- .NET領域驅動設計實戰系列
- .NET領域驅動設計實戰系列 專題一:前期準備之EF CodeFirst
- .NET領域驅動設計實戰系列 專題二:結合領域驅動設計的面向服務架構來搭建網上書店
- .NET領域驅動設計實戰系列 專題三:前期準備之規約模式(Specification Pattern)
- .NET領域驅動設計實戰系列 專題四:前期準備之工作單元模式(Unit Of Work)
- .NET領域驅動設計實戰系列 專題五:網上書店規約模式、工作單元模式的引入以及購物車的實現
- .NET領域驅動設計實戰系列 專題六:DDD實踐案例:網上書店訂單功能的實現
- .NET領域驅動設計實戰系列 專題七:DDD實踐案例:引入事件驅動與中間件機制來實現后臺管理功能
- .NET領域驅動設計實戰系列 專題八:DDD案例:網上書店分布式消息隊列和分布式緩存的實現
- .NET領域驅動設計實戰系列 專題九:DDD案例:網上書店AOP和站點地圖的實現
- .NET領域驅動設計實戰系列 專題十:DDD擴展內容:全面剖析CQRS模式實現
- .NET領域驅動設計實戰系列 專題十一:.NET 領域驅動設計實戰系列總結