由於enode框架的思想是,一次修改只能新建或修改一個聚合根;那麼,若是一個用戶請求要涉及多個聚合根的新建或修改該怎麼辦呢?本文的目的就是要分析清楚這個問題在enode框架下是如何解決的。若是想直接經過看代碼的朋友,能夠直接下載源代碼,源碼中共有三個例子,BankTransferSagaSample這個例子就是本文所用的例子。node
saga的由來web
saga這個術語,可能不少人都還很陌生。saga的提出,最先是爲了解決可能會長時間運行的分佈式事務(long-running process)的問題。所謂long-running的分佈式事務,是指那些企業業務流程,須要跨應用、跨企業來完成某個事務,甚至在事務流程中還須要有手工操做的參與,這類事務的完成時間可能以分計,以小時計,甚至可能以天計。這類事務若是按照事務的ACID的要求去設計,勢必形成系統的可用性大大的下降。試想一個由兩臺服務器一塊兒參與的事務,服務器A發起事務,服務器B參與事務,B的事務須要人工參與,因此處理時間可能很長。若是按照ACID的原則,要保持事務的隔離性、一致性,服務器A中發起的事務中使用到的事務資源將會被鎖定,不容許其餘應用訪問到事務過程當中的中間結果,直到整個事務被提交或者回滾。這就形成事務A中的資源被長時間鎖定,系統的可用性將不可接受。服務器
而saga,則是一種基於補償的消息驅動的用於解決long-running process的一種解決方案。目標是爲了在確保系統高可用的前提下儘可能確保數據的一致性。仍是上面的例子,若是用saga來實現,那就是這樣的流程:服務器A的事務先執行,若是執行順利,那麼事務A就先行提交;若是提交成功,那麼就開始執行事務B,若是事務B也執行順利,則事務B也提交,整個事務就算完成。可是若是事務B執行失敗,那事務B自己須要回滾,這時由於事務A已經提交,因此須要執行一個補償操做,將已經提交的事務A執行的操做做反操做,恢復到未執行前事務A的狀態。這樣的基於消息驅動的實現思路,就是saga。咱們能夠看出,saga是犧牲了數據的強一致性,僅僅實現了最終一致性,可是提升了系統總體的可用性。架構
CQRS架構下的saga (process manager)併發
上面一段,咱們知道了saga的由來,如今咱們再看一下CQRS架構下,saga是用來作什麼的。雖然都叫saga,可是實際上在CQRS架構下,人們每每用saga來解決DDD中多個聚合或多個bounded context之間的通訊問題。DDD中有bounded context的概念。一個bounded context表明一個上下文邊界,一個bounded context中可能包含一個或多個聚合。而saga就是用來實現bounded context之間的通訊,或者是聚合之間的通訊。在經典DDD中,咱們一般用領域服務來實現多個聚合的協調,並最終經過事務的方式來提交全部聚合的修改;這樣作的後果是,1:用到了事務;2.一個事務涉及了多個聚合的更改;這樣作沒什麼很差,在條件容許的狀況下(好比不會出現分佈式事務的狀況下或者併發修改的請求數不高的狀況下),這樣作沒什麼特別大的問題。惟一的問題是,這樣作會增長併發衝突的概率。如今的web應用,每每都是多用戶在同時向系統發送各類處理請求,因此咱們不難想到,一個事務中涉及到的聚合越多,那併發衝突的可能性就越高。不只如此,若是你的聚合很大,包含了不少的子實體和不少的方法,那該聚合持久化時產生併發衝突的概率也會相對較高;而系統的併發衝突將直接影響系統的可用性;因此,通常的建議是,咱們應該儘可能將聚合設計的小,且儘可能一次只修改一個聚合;這樣咱們就不須要事務,還能把併發衝突的可能性降到最低;固然,單個聚合持久化時也還會存在併發衝突,但這點相對很容易解決,由於單個聚合是數據一致性的最小單元,因此咱們能夠徹底不須要事務,經過樂觀鎖就能解決併發覆蓋的問題;關於這個問題的討論,你們若是還有興趣或者想了解的更加深刻,我推薦看一下Effective Aggregate Design這篇論文,共三個部分,其做者是《implementing domain-driven design》一書的做者。框架
因此,經過上面的分析,咱們知道了「聚合應該設計的小,且一次修改只修改一個聚合」這樣一條不成文的原則。固然你必定有不少理由認爲不該該這樣,歡迎你們討論。那麼若是要遵循這樣的原則,那咱們須要一種機制來解決多個聚合之間的通訊的問題。在CQRS的架構下,人們也都把這種機制叫作saga,但由於這種CQRS架構下的saga的語義已經不是上面一段中介紹的saga了。因此,微軟也把cqrs架構下的saga叫作process manager,具體能夠看一下微軟的一個CQRS架構的開源項目的例子;process manager這個名字咱們應該很容易理解,即流程管理器。事實上,一個saga所作的事情就是和一個流程同樣的事情。只不過傳統的流程,都有一個流程定義,當用戶發起一個流程時,就會產生一個流程實例,該流程實例會嚴格按照流程定義的流向來進行流轉,驅動流程流轉的每每是人的操做,好比審批操做。而process manager,也是一個流程,只不過這個流程是由消息驅動的。一個典型的process manager會響應事件,而後產生新的命令去執行下一步操做。用過NServiceBus的人應該知道,NServiceBus中就內置了saga的機制,咱們能夠很輕鬆的利用它來實現分佈式的消息驅動的long-running process;dom
如何用enode框架來實現saga異步
爲了說明問題,我就以經典的銀行轉帳爲例子來說解吧,由於轉帳的核心業務你們都很清楚,因此咱們就沒有了業務上理解的不一致,咱們就能專心思考如何實現的問題了。可是,爲了便於下面的分析,我仍是簡單定義一下本例中的銀行轉帳的核心業務流程。注意:實際的轉帳業務流程遠比我定義的複雜,我這裏重點是爲了分析如何實現一個會涉及多個聚合修改的的業務場景。核心業務描述以下:分佈式
兩個銀行帳號,一個是源帳號,一個是目標帳號;函數
用戶點擊確認轉帳按鈕後,指定數目的錢會從源帳號轉入到目標帳號;
整個轉帳過程有兩個階段:1)錢從源帳號轉出;2)錢轉入到目標帳號;若是一切順利,那轉帳流程就結束了;
若是源帳號的當前餘額不足,則轉出操做會失敗,系統記錄錯誤日誌,轉帳流程結束;
若是錢轉入到目標帳號時出現異常,則須要回滾源帳號已轉出的錢,同時記錄錯誤日誌,回滾完成後,轉帳流程結束;
思路分析:
經過上面的需求,咱們知道,應該有一個聚合根,表示銀行帳號,我設計爲BankAccount;BankAccount有轉出錢和轉入錢的行爲職責。另外,根據上面第5條需求,BankAccount可能會有回滾轉出錢的行爲職責;另外,固然一個銀行帳號還會有一個表示當前餘額的狀態屬性;
因爲咱們是經過saga的思想來實現轉帳流程,那咱們具體該如何設計此saga呢?saga在CQRS架構中的做用是響應事件,產生command,從而起到以事件消息驅動的原理來控制流程流轉的做用;轉帳流程如何才能結束會由saga來決定。那麼saga要響應的事件哪裏來呢?很明顯,就是從流程中涉及到的聚合根裏來,本例就是響應BankAccount的事件;當BankAccount的轉出事件或轉入事件發生後,會被saga響應,而後saga會作出響應,決定下一步該怎麼走。
saga是聚合根嗎?或者說saga屬於領域層的東西嗎?這個問題很重要,我以爲沒有明確的答案。並且我也沒有從各類資料上明確看到saga是屬於ddd中的應用層仍是領域層仍是其餘層。如下是我我的的一些思考:
關於認爲saga應該屬於領域層的緣由的一些思考:和經典的DDD作類比,經典DDD的書本上,會有一個銀行轉帳的領域服務,該領域服務完成轉帳的核心業務邏輯;而一些外圍的邏輯,如記錄日誌、發送郵件或短信通知等功能,則在外圍的應用層服務中作掉;因此按照這個理解,假如咱們設計一個saga,來實現轉帳的核心業務邏輯,那我以爲saga也是一個聚合根。由於saga是一個流程,職責是控制轉帳的過程,它有一個全局惟一的流程ID(一次轉帳就會產生一個轉帳流程,流程ID是流程的全局惟一標識),屬於領域層,saga能夠理解爲是領域中對行爲過程的建模。固然saga與普通的聚合根稍有區別,普通的聚合根咱們一般是根據名詞法則去識別,而saga則是從交互行爲或者流程的角度去識別;這點就比如經典DDD的領域模型中有聚合根和領域服務同樣,聚合根是數據的建模,領域服務是交互行爲的建模;
關於認爲saga不該該屬於領域層的緣由的一些思考:按照saga在CQRS架構下的定義,它會接受響應event,而後發送command。而command是應用層的東西,因此就會致使domain層依賴於應用層,顯然不太合理;
關於認爲saga不該該屬於應用層的緣由的一些思考:由於saga是流程,且有流程狀態,有狀態就須要保存,這樣就變成應用層中的saga須要保存狀態,這種作法合理嗎?值得咱們深思;另外,按照經典DDD的架構中對應用層的職責定義,應用層應該是很薄的,更加不會出現須要保存狀態的屬於應用層的對象;
經過上面第3點的一些討論,我我的會把saga設計在領域層,設計爲聚合根,可是,我會對saga的實現作一些調整:1)saga聚合根不會直接響應事件,而是通過一箇中間command來過分;2)saga聚合根也不會直接發送command,取而代之的是像普通聚合根同樣也產生事件,這種事件表達的意思是「發送某某command的意圖已下達」,而後外層的event handler接受到這樣的事件後,會發送對應的command給command service;這樣一來,saga聚合根就和普通的聚合根無任何差異,聽上去感受很難以想象,稍後咱們結合代碼一塊兒看一下具體實現吧。
若是saga也是一個聚合根,那不是和BankAccount平級了,那BankAccount產生的事件如何傳遞給saga呢?顯然,咱們還缺乏同樣東西,就是須要把流程中涉及到修改的聚合根產生的事件傳遞給saga聚合根的event handler。這種event handler自己無業務邏輯,他們的職責是監聽聚合根產生的事件,而後將event轉化爲command,而後將command發送到command service,從而最後通知到對應的saga,而後saga就開始處理該事件,好比決定接下來該如何處理;
代碼實現:
BankAccount聚合根的設計
/// <summary>銀行帳號聚合根 /// </summary> [Serializable] public class BankAccount : AggregateRoot<Guid>, IEventHandler<AccountOpened>, //銀行帳戶已開 IEventHandler<Deposited>, //錢已存入 IEventHandler<TransferedOut>, //錢已轉出 IEventHandler<TransferedIn>, //錢已轉入 IEventHandler<TransferOutRolledback> //轉出已回滾 { /// <summary>帳號(卡號) /// </summary> public string AccountNumber { get; private set; } /// <summary>擁有者 /// </summary> public string Owner { get; private set; } /// <summary>當前餘額 /// </summary> public double Balance { get; private set; } public BankAccount() : base() { } public BankAccount(Guid accountId, string accountNumber, string owner) : base(accountId) { RaiseEvent(new AccountOpened(Id, accountNumber, owner)); } /// <summary>存款 /// </summary> /// <param name="amount"></param> public void Deposit(double amount) { RaiseEvent(new Deposited(Id, amount, string.Format("向帳戶{0}存入金額{1}", AccountNumber, amount))); } /// <summary>轉出 /// </summary> /// <param name="targetAccount"></param> /// <param name="processId"></param> /// <param name="transferInfo"></param> public void TransferOut(BankAccount targetAccount, Guid processId, TransferInfo transferInfo) { //這裏判斷當前餘額是否足夠 if (Balance < transferInfo.Amount) { throw new Exception(string.Format("帳戶{0}餘額不足,不能轉帳!", AccountNumber)); } RaiseEvent(new TransferedOut(processId, transferInfo, string.Format("{0}向帳戶{1}轉出金額{2}", AccountNumber, targetAccount.AccountNumber, transferInfo.Amount))); } /// <summary>轉入 /// </summary> /// <param name="sourceAccount"></param> /// <param name="processId"></param> /// <param name="transferInfo"></param> public void TransferIn(BankAccount sourceAccount, Guid processId, TransferInfo transferInfo) { RaiseEvent(new TransferedIn(processId, transferInfo, string.Format("{0}從帳戶{1}轉入金額{2}", AccountNumber, sourceAccount.AccountNumber, transferInfo.Amount))); } /// <summary>回滾轉出 /// </summary> /// <param name="processId"></param> /// <param name="transferInfo"></param> public void RollbackTransferOut(Guid processId, TransferInfo transferInfo) { RaiseEvent(new TransferOutRolledback(processId, transferInfo, string.Format("帳戶{0}取消轉出金額{1}", AccountNumber, transferInfo.Amount))); } void IEventHandler<AccountOpened>.Handle(AccountOpened evnt) { AccountNumber = evnt.AccountNumber; Owner = evnt.Owner; } void IEventHandler<Deposited>.Handle(Deposited evnt) { Balance += evnt.Amount; } void IEventHandler<TransferedOut>.Handle(TransferedOut evnt) { Balance -= evnt.TransferInfo.Amount; } void IEventHandler<TransferedIn>.Handle(TransferedIn evnt) { Balance += evnt.TransferInfo.Amount; } void IEventHandler<TransferOutRolledback>.Handle(TransferOutRolledback evnt) { Balance += evnt.TransferInfo.Amount; } }
轉帳流程TransferProcess聚合根的設計
/// <summary>轉帳流程狀態 /// </summary> public enum ProcessState { NotStarted, Started, TransferOutRequested, TransferInRequested, RollbackTransferOutRequested, Completed, Aborted } /// <summary>轉帳信息值對象,包含了轉帳的基本信息 /// </summary> [Serializable] public class TransferInfo { public Guid SourceAccountId { get; private set; } public Guid TargetAccountId { get; private set; } public double Amount { get; private set; } public TransferInfo(Guid sourceAccountId, Guid targetAccountId, double amount) { SourceAccountId = sourceAccountId; TargetAccountId = targetAccountId; Amount = amount; } } /// <summary>銀行轉帳流程聚合根,負責控制整個轉帳的過程,包括遇到異常時的回滾處理 /// </summary> [Serializable] public class TransferProcess : AggregateRoot<Guid>, IEventHandler<TransferProcessStarted>, //轉帳流程已開始 IEventHandler<TransferOutRequested>, //轉出的請求已發起 IEventHandler<TransferInRequested>, //轉入的請求已發起 IEventHandler<RollbackTransferOutRequested>, //回滾轉出的請求已發起 IEventHandler<TransferProcessCompleted>, //轉帳流程已正常完成 IEventHandler<TransferProcessAborted> //轉帳流程已異常終止 { /// <summary>當前轉帳流程狀態 /// </summary> public ProcessState State { get; private set; } public TransferProcess() : base() { } public TransferProcess(BankAccount sourceAccount, BankAccount targetAccount, TransferInfo transferInfo) : base(Guid.NewGuid()) { RaiseEvent(new TransferProcessStarted(Id, transferInfo, string.Format("轉帳流程啓動,源帳戶:{0},目標帳戶:{1},轉帳金額:{2}", sourceAccount.AccountNumber, targetAccount.AccountNumber, transferInfo.Amount))); RaiseEvent(new TransferOutRequested(Id, transferInfo)); } /// <summary>處理已轉出事件 /// </summary> /// <param name="transferInfo"></param> public void HandleTransferedOut(TransferInfo transferInfo) { RaiseEvent(new TransferInRequested(Id, transferInfo)); } /// <summary>處理已轉入事件 /// </summary> /// <param name="transferInfo"></param> public void HandleTransferedIn(TransferInfo transferInfo) { RaiseEvent(new TransferProcessCompleted(Id, transferInfo)); } /// <summary>處理轉出失敗的狀況 /// </summary> /// <param name="transferInfo"></param> public void HandleFailedTransferOut(TransferInfo transferInfo) { RaiseEvent(new TransferProcessAborted(Id, transferInfo)); } /// <summary>處理轉入失敗的狀況 /// </summary> /// <param name="transferInfo"></param> public void HandleFailedTransferIn(TransferInfo transferInfo) { RaiseEvent(new RollbackTransferOutRequested(Id, transferInfo)); } /// <summary>處理轉出已回滾事件 /// </summary> /// <param name="transferInfo"></param> public void HandleTransferOutRolledback(TransferInfo transferInfo) { RaiseEvent(new TransferProcessAborted(Id, transferInfo)); } void IEventHandler<TransferProcessStarted>.Handle(TransferProcessStarted evnt) { State = ProcessState.Started; } void IEventHandler<TransferOutRequested>.Handle(TransferOutRequested evnt) { State = ProcessState.TransferOutRequested; } void IEventHandler<TransferInRequested>.Handle(TransferInRequested evnt) { State = ProcessState.TransferInRequested; } void IEventHandler<RollbackTransferOutRequested>.Handle(RollbackTransferOutRequested evnt) { State = ProcessState.RollbackTransferOutRequested; } void IEventHandler<TransferProcessCompleted>.Handle(TransferProcessCompleted evnt) { State = ProcessState.Completed; } void IEventHandler<TransferProcessAborted>.Handle(TransferProcessAborted evnt) { State = ProcessState.Aborted; } }
響應BankAccount聚合根所發生的事件的event handler設計
/// <summary>事件訂閱者,用於監聽和響應銀行帳號聚合根產生的事件 /// </summary> public class BankAccountEventHandler : IEventHandler<AccountOpened>, //銀行帳戶已開 IEventHandler<Deposited>, //錢已存入 IEventHandler<TransferedOut>, //錢已轉出 IEventHandler<TransferedIn>, //錢已轉入 IEventHandler<TransferOutRolledback> //轉出已回滾 { private ICommandService _commandService; public BankAccountEventHandler(ICommandService commandService) { _commandService = commandService; } void IEventHandler<AccountOpened>.Handle(AccountOpened evnt) { Console.WriteLine(string.Format("建立銀行帳戶{0}", evnt.AccountNumber)); } void IEventHandler<Deposited>.Handle(Deposited evnt) { Console.WriteLine(evnt.Description); } void IEventHandler<TransferedOut>.Handle(TransferedOut evnt) { Console.WriteLine(evnt.Description); //響應已轉出事件,發送「處理已轉出事件」的命令 _commandService.Send(new HandleTransferedOut { ProcessId = evnt.ProcessId, TransferInfo = evnt.TransferInfo }); } void IEventHandler<TransferedIn>.Handle(TransferedIn evnt) { Console.WriteLine(evnt.Description); //響應已轉入事件,發送「處理已轉入事件」的命令 _commandService.Send(new HandleTransferedIn { ProcessId = evnt.ProcessId, TransferInfo = evnt.TransferInfo }); } void IEventHandler<TransferOutRolledback>.Handle(TransferOutRolledback evnt) { Console.WriteLine(evnt.Description); //響應轉出已回滾事件,發送「處理轉出已回滾事件」的命令 _commandService.Send(new HandleTransferOutRolledback { ProcessId = evnt.ProcessId, TransferInfo = evnt.TransferInfo }); } }
響應TransferProcess聚合根所發生的事件的event handler設計
/// <summary>事件訂閱者,用於監聽和響應轉帳流程聚合根產生的事件 /// </summary> public class TransferProcessEventHandler : IEventHandler<TransferProcessStarted>, //轉帳流程已開始 IEventHandler<TransferOutRequested>, //轉出的請求已發起 IEventHandler<TransferInRequested>, //轉入的請求已發起 IEventHandler<RollbackTransferOutRequested>, //回滾轉出的請求已發起 IEventHandler<TransferProcessCompleted>, //轉帳流程已完成 IEventHandler<TransferProcessAborted> //轉帳流程已終止 { private ICommandService _commandService; public TransferProcessEventHandler(ICommandService commandService) { _commandService = commandService; } void IEventHandler<TransferProcessStarted>.Handle(TransferProcessStarted evnt) { Console.WriteLine(evnt.Description); } void IEventHandler<TransferOutRequested>.Handle(TransferOutRequested evnt) { //響應「轉出的命令請求已發起」這個事件,發送「轉出」命令 _commandService.Send(new TransferOut { ProcessId = evnt.ProcessId, TransferInfo = evnt.TransferInfo }, (result) => { //這裏是command的異步回調函數,若是有異常,則發送「處理轉出失敗」的命令 if (result.Exception != null) { Console.WriteLine(result.Exception.Message); _commandService.Send(new HandleFailedTransferOut { ProcessId = evnt.ProcessId, TransferInfo = evnt.TransferInfo }); } }); } void IEventHandler<TransferInRequested>.Handle(TransferInRequested evnt) { //響應「轉入的命令請求已發起」這個事件,發送「轉入」命令 _commandService.Send(new TransferIn { ProcessId = evnt.ProcessId, TransferInfo = evnt.TransferInfo }, (result) => { //這裏是command的異步回調函數,若是有異常,則發送「處理轉入失敗」的命令 if (result.Exception != null) { Console.WriteLine(result.Exception.Message); _commandService.Send(new HandleFailedTransferIn { ProcessId = evnt.ProcessId, TransferInfo = evnt.TransferInfo }); } }); } void IEventHandler<RollbackTransferOutRequested>.Handle(RollbackTransferOutRequested evnt) { //響應「回滾轉出的命令請求已發起」這個事件,發送「回滾轉出」命令 _commandService.Send(new RollbackTransferOut { ProcessId = evnt.ProcessId, TransferInfo = evnt.TransferInfo }); } void IEventHandler<TransferProcessCompleted>.Handle(TransferProcessCompleted evnt) { Console.WriteLine("轉帳流程已正常完成!"); } void IEventHandler<TransferProcessAborted>.Handle(TransferProcessAborted evnt) { Console.WriteLine("轉帳流程已異常終止!"); } }
BankAccount聚合根相關的command handlers
/// <summary>銀行帳戶相關命令處理 /// </summary> public class BankAccountCommandHandlers : ICommandHandler<OpenAccount>, //開戶 ICommandHandler<Deposit>, //存錢 ICommandHandler<TransferOut>, //轉出 ICommandHandler<TransferIn>, //轉入 ICommandHandler<RollbackTransferOut> //回滾轉出 { public void Handle(ICommandContext context, OpenAccount command) { context.Add(new BankAccount(command.AccountId, command.AccountNumber, command.Owner)); } public void Handle(ICommandContext context, Deposit command) { context.Get<BankAccount>(command.AccountId).Deposit(command.Amount); } public void Handle(ICommandContext context, TransferOut command) { var sourceAccount = context.Get<BankAccount>(command.TransferInfo.SourceAccountId); var targetAccount = context.Get<BankAccount>(command.TransferInfo.TargetAccountId); sourceAccount.TransferOut(targetAccount, command.ProcessId, command.TransferInfo); } public void Handle(ICommandContext context, TransferIn command) { var sourceAccount = context.Get<BankAccount>(command.TransferInfo.SourceAccountId); var targetAccount = context.Get<BankAccount>(command.TransferInfo.TargetAccountId); targetAccount.TransferIn(sourceAccount, command.ProcessId, command.TransferInfo); } public void Handle(ICommandContext context, RollbackTransferOut command) { context.Get<BankAccount>(command.TransferInfo.SourceAccountId).RollbackTransferOut(command.ProcessId, command.TransferInfo); } }
TransferProcess聚合根相關的command handlers
/// <summary>銀行轉帳流程相關命令處理 /// </summary> public class TransferProcessCommandHandlers : ICommandHandler<StartTransfer>, //開始轉帳 ICommandHandler<HandleTransferedOut>, //處理已轉出事件 ICommandHandler<HandleTransferedIn>, //處理已轉入事件 ICommandHandler<HandleFailedTransferOut>, //處理轉出失敗 ICommandHandler<HandleFailedTransferIn>, //處理轉入失敗 ICommandHandler<HandleTransferOutRolledback> //處理轉出已回滾事件 { public void Handle(ICommandContext context, StartTransfer command) { var sourceAccount = context.Get<BankAccount>(command.TransferInfo.SourceAccountId); var targetAccount = context.Get<BankAccount>(command.TransferInfo.TargetAccountId); context.Add(new TransferProcess(sourceAccount, targetAccount, command.TransferInfo)); } public void Handle(ICommandContext context, HandleTransferedOut command) { context.Get<TransferProcess>(command.ProcessId).HandleTransferedOut(command.TransferInfo); } public void Handle(ICommandContext context, HandleTransferedIn command) { context.Get<TransferProcess>(command.ProcessId).HandleTransferedIn(command.TransferInfo); } public void Handle(ICommandContext context, HandleFailedTransferOut command) { context.Get<TransferProcess>(command.ProcessId).HandleFailedTransferOut(command.TransferInfo); } public void Handle(ICommandContext context, HandleFailedTransferIn command) { context.Get<TransferProcess>(command.ProcessId).HandleFailedTransferIn(command.TransferInfo); } public void Handle(ICommandContext context, HandleTransferOutRolledback command) { context.Get<TransferProcess>(command.ProcessId).HandleTransferOutRolledback(command.TransferInfo); } }