带有async / await的ICommandHandler / IQueryHandler

时间:2014-01-08 00:41:37

标签: asp.net-mvc async-await cqrs c#-5.0 simple-injector

EDITH说(tl; dr)

我选择了建议解决方案的变体;保持所有ICommandHandlerIQueryHandler可能是异步的,并在同步情况下返回已解析的任务。尽管如此,我还是不想在整个地方使用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
    }
}

我对这种方法的一些观察:

  1. 在纯CQS中,只有查询应返回值,而命令应该只返回命令。实际上,命令返回值而不是发出命令并且稍后对命令应该首先返回的事物(例如数据库ID等)进行查询更方便。这就是the author suggested将返回值放入命令POCO的原因。
  2. 从命令返回的内容不是很明显,事实上它看起来就像命令是一个火灾和遗忘类型的东西,直到你最终遇到处理程序后访问的奇怪的结果属性已经运行加上命令现在知道它的结果
  3. 处理程序具有以使其同步 - 查询和命令。事实证明,使用C#5.0,您可以在您最喜欢的DI容器的帮助下注入async/await驱动的处理程序,但是编译器在编译时并不知道这一点,因此MVC处理程序将会失败。异常,告诉您在所有异步任务完成执行之前返回的方法。
  4. 当然你可以将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 - 这很漂亮丑
    • 我可以提供处理程序接口的同步和异步版本(或者将它们全部放在一个膨胀实现的接口中)但我的理解是,理想情况下,处理程序的使用者不应该意识到这一事实命令/查询处理程序是同步或异步的,因为这是一个跨领域的问题。如果我需要使以前的同步命令异步怎么办?我必须改变处理程序的每个消费者,这可能会破坏我在代码中的语义。
    • 另一方面,潜在的 -async-handlers-approach甚至可以让我能够通过在我的DI容器的帮助下修改同步处理程序来实现异步

    现在我没有看到最佳解决方案......我不知所措。

    任何有类似问题和优雅解决方案的人我都没有想到?

4 个答案:

答案 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 HandleTask 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>分别返回TaskTask<T>。在同步实现的情况下,我使用Task.FromResult(...)。我也有一些*处理程序装饰器(比如用于记录),你可以想象这些也需要改变。

现在,我决定使'一切'可能等待,并养成了将await与我的调度程序结合使用的习惯(在ninject内核中找到处理程序并调用句柄)。

我一直都是异步,也是在我的webapi / mvc控制器中,除了少数例外。在极少数情况下,我使用Continuewith(...)Wait()将事物包装在同步方法中。

另一个相关的挫折感是,MR建议使用* Async后缀命名方法,以防它们是(duh)async。但由于这是一个实施决定,我(现在)决定坚持Handle(...)而不是HandleAsync(...)

这绝对不是一个令人满意的结果,我也在寻找更好的解决方案。