帶非同步/等待的 ICommandHandler/IQueryHandler
伊迪絲說(tl;博士)
我選擇了建議解決方案的變體;保持所有
ICommandHandlers 和IQueryHandlers 可能是非同步的,並在同步情況下返回已解決的任務。不過,我不想到處使用Task.FromResult(...),所以為了方便起見,我定義了一個擴展方法:public static class TaskExtensions { public static Task<TResult> AsTaskResult<TResult>(this TResult result) { // Or TaskEx.FromResult if you're targeting .NET4.0 // with the Microsoft.BCL.Async package return Task.FromResult(result); } } // Usage in code ... using TaskExtensions; class MySynchronousQueryHandler : IQueryHandler<MyQuery, bool> { public Task<bool> Handle(MyQuery query) { return true.AsTaskResult(); } } class MyAsynchronousQueryHandler : IQueryHandler<MyQuery, bool> { public async Task<bool> Handle(MyQuery query) { return await this.callAWebserviceToReturnTheResult(); } }遺憾的是,C# 不是Haskell ……但 8-)。真的聞起來像Arrows的應用程序。無論如何,希望這對任何人都有幫助。現在到我原來的問題:-)
介紹
你好呀!
對於一個項目,我目前正在用 C#(.NET4.5、C#5.0、ASP.NET MVC4)設計應用程序架構。有了這個問題,我希望就我偶然發現的一些問題獲得一些意見
async/await。注意:這是一個相當長的一個:-)我的解決方案結構如下所示:
MyCompany.Contract(命令/查詢和通用介面)MyCompany.MyProject(包含業務邏輯和命令/查詢處理程序)MyCompany.MyProject.Web(MVC 網路前端)我閱讀了可維護架構和命令-查詢-分離,發現這些文章非常有幫助:
到目前為止,我已經了解
ICommandHandler/IQueryHandler概念和依賴注入(我正在使用SimpleInjector - 它真的很簡單)。給定的方法
上述文章的方法建議使用 POCO 作為命令/查詢,並將這些的調度程序描述為以下處理程序介面的實現:
interface IQueryHandler<TQuery, TResult> { TResult Handle(TQuery query); } interface ICommandHandler<TCommand> { void Handle(TCommand command); }在 MVC 控制器中,您將按如下方式使用它:
class AuthenticateCommand { // The token to use for authentication public string Token { get; set; } public string SomeResultingSessionId { get; set; } } class AuthenticateController : Controller { private readonly ICommandHandler<AuthenticateCommand> authenticateUser; public AuthenticateController(ICommandHandler<AuthenticateCommand> authenticateUser) { // Injected via DI container this.authenticateUser = authenticateUser; } public ActionResult Index(string externalToken) { var command = new AuthenticateCommand { Token = externalToken }; this.authenticateUser.Handle(command); var sessionId = command.SomeResultingSessionId; // Do some fancy thing with our new found knowledge } }我對這種方法的一些觀察:
- 在純CQS中,只有查詢應該返回值,而命令應該是,只有命令。實際上,命令返回值而不是發出命令然後查詢命令應該首先返回的東西(例如數據庫ID等)更方便。這就是為什麼作者建議將返回值放入命令 POCO 中。
- 從命令返回的內容不是很明顯,事實上,它看起來命令是一個火災並忘記類型的東西,直到你最終遇到在處理程序執行後被訪問的奇怪結果屬性加上命令現在知道它的結果
- 處理程序必須同步才能工作 - 查詢和命令。正如 C#5.0 所證明的那樣,您可以
async/await在您最喜歡的 DI 容器的幫助下注入動力處理程序,但編譯器在編譯時並不知道這一點,因此 MVC 處理程序將失敗並出現異常,告訴您方法返回在所有非同步任務完成執行之前。當然,您可以將 MVC 處理程序標記為
async,這就是這個問題的意義所在。命令返回值
我考慮了給定的方法並對介面進行了更改以解決問題 1. 和 2. 我添加了一個
ICommandHandler具有顯式結果類型的介面 - 就像IQueryHandler. 這仍然違反了 CQS,但至少很明顯,這些命令返回了某種值,另外還有一個好處是不必用 result 屬性來弄亂命令對象:interface ICommandHandler<TCommand, TResult> { TResult Handle(TCommand command); }自然有人會爭辯說,當您擁有相同的命令和查詢界面時,何必費心呢?但我認為值得以不同的方式命名它們——只是在我看來更乾淨。
我的初步解決方案
然後我認真思考了手頭的第三個問題……我的一些命令/查詢處理程序需要是非同步的(例如,
WebRequest向另一個 Web 服務發出一個進行身份驗證),其他的則不需要。所以我認為最好從頭開始設計我的處理程序async/await——這當然會冒泡到 MVC 處理程序,即使對於實際上是同步的處理程序也是如此:interface IQueryHandler<TQuery, TResult> { Task<TResult> Handle(TQuery query); } interface ICommandHandler<TCommand> { Task Handle(TCommand command); } interface ICommandHandler<TCommand, TResult> { Task<TResult> Handle(TCommand command); } class AuthenticateCommand { // The token to use for authentication public string Token { get; set; } // No more return properties ... }驗證控制器:
class AuthenticateController : Controller { private readonly ICommandHandler<AuthenticateCommand, string> authenticateUser; public AuthenticateController(ICommandHandler<AuthenticateCommand, string> authenticateUser) { // Injected via DI container this.authenticateUser = authenticateUser; } public async Task<ActionResult> Index(string externalToken) { var command = new AuthenticateCommand { Token = externalToken }; // It's pretty obvious that the command handler returns something var sessionId = await this.authenticateUser.Handle(command); // Do some fancy thing with our new found knowledge } }
async儘管這解決了我的問題 - 明顯的返回值,所有處理程序都可以是非同步的 -僅僅因為放置一個不是非同步的東西會傷害我的大腦。我看到這個有幾個缺點:
- 處理程序介面並不像我希望的那樣整潔 -
Task<...>我眼中的東西非常冗長,乍一看混淆了我只想從查詢/命令返回一些東西的事實- 編譯器會警告您在同步處理程序實現中沒有適當
await的(我希望能夠編譯我Release的 withWarnings as Errors) - 您可以用 pragma 覆蓋它……是的……好吧……- 在這些情況下,我可以省略
async關鍵字以使編譯器滿意,但為了實現處理程序介面,您必須Task顯式返回某種類型 - 這非常難看- 我可以提供處理程序介面的同步和非同步版本(或將它們全部放在一個使實現膨脹的介面中),但我的理解是,理想情況下,處理程序的使用者不應該知道命令/查詢的事實處理程序是同步或非同步的,因為這是一個橫切關注點。如果我需要使以前的同步命令非同步怎麼辦?我必須更改處理程序的每個使用者,這可能會在我通過程式碼的過程中破壞語義。
- 另一方面,潛在的-async-handlers-approach 甚至可以讓我通過在我的 DI 容器的幫助下裝飾同步處理程序來將它們更改為非同步
現在我沒有看到最好的解決方案……我很茫然。
有人有類似的問題和我沒想到的優雅解決方案嗎?
Async 和 await 不能與傳統的 OOP 完美結合。我有一個關於這個主題的部落格系列;您可能會發現有關非同步介面的文章特別有用(儘管我沒有涵蓋您尚未發現的任何內容)。
周圍的設計問題與周圍
async的問題極為相似IDisposable;添加到介面是一項重大更改IDisposable,因此您需要知道任何可能的實現是否可能是一次性的(實現細節)。存在一個並行問題async;您需要知道任何可能的實現是否可能是非同步的(實現細節)。由於這些原因,我
Task將介面上的 -returning 方法視為“可能是非同步的”方法,就像介面繼承自IDisposable意味著它“可能擁有資源”一樣。我所知道的最好的方法是:
Task使用非同步簽名(返回/ )定義任何可能非同步的方法Task<T>。- 返回
Task.FromResult(...)同步實現。這比async沒有 a更合適await。這種方法幾乎正是您已經在做的。對於純函式式語言,可能存在更理想的解決方案,但我看不到 C# 的解決方案。