我选择了建议解决方案的变体;保持所有ICommandHandler
和IQueryHandler
可能是异步的,并在同步情况下返回已解析的任务。尽管如此,我还是不想在整个地方使用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网络前端)我阅读了可维护的架构和Command-Query-Separation,发现这些帖子非常有用:
到目前为止,我已经了解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
}
}
我对这种方法的一些观察:
async/await
驱动的处理程序,但是编译器在编译时并不知道这一点,因此MVC处理程序将会失败。异常,告诉您在所有异步任务完成执行之前返回的方法。 当然你可以将MVC处理程序标记为async
,这就是这个问题的关键。
我考虑了给定的方法并对接口进行了更改以解决问题1.和2.我添加了一个具有显式结果类型的ICommandHandler
- 就像IQueryHandler
一样。这仍然违反了CQS,但至少很明显,这些命令会返回某种值,而且还有一个额外的好处,即不必使用result属性来混淆命令对象:
interface ICommandHandler<TCommand, TResult>
{
TResult Handle(TCommand command);
}
当然有人可能会争辩说,当你拥有相同的命令和查询界面时,为什么要这么麻烦?但我认为值得为它们命名不同 - 只是看起来更清洁。
然后我想到了第3个问题......我的一些命令/查询处理程序需要是异步的(例如,向另一个Web服务发出WebRequest
进行身份验证)其他人不会。所以我认为最好从头开始为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 ...
}
AuthenticateController:
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<...>
东西非常冗长,乍一看似乎混淆了我只想从查询/命令中返回一些东西< / LI>
await
(我希望能够使用Release
编译我的Warnings as Errors
) - 您可以用pragma覆盖它......是啊......好吧...... async
关键字以使编译器满意,但为了实现处理程序接口,您必须明确地返回某种Task
- 这很漂亮丑现在我没有看到最佳解决方案......我不知所措。
任何有类似问题和优雅解决方案的人我都没有想到?
答案 0 :(得分:15)
Async和await不能与传统的OOP完美搭配。我有一个关于这个主题的博客系列;您可能会发现post on async interfaces特别有用(尽管我没有涵盖您尚未发现的任何内容)。
async
周围的设计问题与IDisposable
周围的设计问题非常相似;将IDisposable
添加到接口是一个重大变化,因此您需要知道任何可能的实现是否永远是一次性的(实现细节)。 async
存在并行问题;您需要知道任何可能的实现是否永远是异步的(实现细节)。
由于这些原因,我查看Task
- 将接口上的方法作为“可能异步”方法返回,就像从IDisposable
继承的接口意味着它“可能拥有资源”。
我所知道的最佳方法是:
Task
/ Task<T>
)。Task.FromResult(...)
以进行同步实施。这比没有async
的{{1}}更合适。这种方法几乎就是你已经在做的。对于纯函数式语言,可能存在更理想的解决方案,但我没有看到C#。
答案 1 :(得分:10)
我为此创建了一个项目 - 我最终没有拆分命令和查询,而是使用请求/响应和pub / sub - https://github.com/jbogard/MediatR
public interface IMediator
{
TResponse Send<TResponse>(IRequest<TResponse> request);
Task<TResponse> SendAsync<TResponse>(IAsyncRequest<TResponse> request);
void Publish<TNotification>(TNotification notification) where TNotification : INotification;
Task PublishAsync<TNotification>(TNotification notification) where TNotification : IAsyncNotification;
}
对于命令不返回结果的情况,我使用了返回Void类型的基类(功能组的单位)。这使我有了一个统一的接口来发送有响应的消息,而null响应是一个显式的返回值。
作为暴露命令的人,您明确选择在请求定义中异步,而不是强迫每个人都是异步。
答案 2 :(得分:9)
你说:
处理程序的消费者不应该意识到a的事实 命令/查询处理程序是同步或异步,因为这是一个交叉 关注Stephen Clearly已经对此有所了解,但异步并不是一个贯穿各领域的问题(或者至少不是它在.NET中实现的方式)。 Async是一个架构问题,因为你必须事先决定是否使用它,它完全有可能你的所有应用程序代码。它改变了你的界面,因此不可能偷偷摸摸&#39;这个,没有应用程序知道它。
虽然.NET使异步更容易,正如你所说,它仍然会伤害你的眼睛和头脑。也许它只需要进行心理训练,但我真的很想知道对大多数应用程序来说是否同样是异步。
无论哪种方式,都要防止为命令处理程序提供两个接口。您必须选择一个,因为有两个单独的接口将强制您复制要应用于它们的所有装饰器并复制您的DI配置。因此要么有一个返回Task
并使用输出属性的接口,要么使用Task<TResut>
并返回某种Void
类型,以防没有返回类型。
你可以想象(你指出的文章是我的)我的个人偏好是使用void Handle
或Task Handle
方法,因为有了命令,重点不在于返回值以及何时如果有一个返回值,您将最终拥有一个重复的接口结构,因为查询具有:
public interface ICommand<TResult> { }
public interface ICommandHandler<TCommand, TResult>
where TCommand : ICommand<TResult>
{
Task<TResult> Handle(TCommand command);
}
如果没有ICommand<TResult>
接口和泛型类型约束,您将缺少编译时支持。这是我在Meanwhile... on the query side of my architecture
答案 3 :(得分:4)
不是真正的答案,但对于它的价值,我得出了完全相同的结论,以及非常类似的实现。
我的ICommandHandler<T>
和IQueryHandler<T>
分别返回Task
和Task<T>
。在同步实现的情况下,我使用Task.FromResult(...)
。我也有一些*处理程序装饰器(比如用于记录),你可以想象这些也需要改变。
现在,我决定使'一切'可能等待,并养成了将await
与我的调度程序结合使用的习惯(在ninject内核中找到处理程序并调用句柄)。
我一直都是异步,也是在我的webapi / mvc控制器中,除了少数例外。在极少数情况下,我使用Continuewith(...)
和Wait()
将事物包装在同步方法中。
另一个相关的挫折感是,MR建议使用* Async后缀命名方法,以防它们是(duh)async。但由于这是一个实施决定,我(现在)决定坚持Handle(...)
而不是HandleAsync(...)
。
这绝对不是一个令人满意的结果,我也在寻找更好的解决方案。