2014-01-08 116 views
14

EDITH说(TL; DR)

我建议的解决方案的一个变体去;保持所有ICommandHandler s和IQueryHandler可能异步并在同步情况下返回已解决的任务。不过,我不希望使用Task.FromResult(...)所有的地方,所以我为了方便定义的扩展方法:ICommandHandler/IQueryHandler与异步/等待

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的Web前端)

我在维护结构和命令查询的Separa读了重刑,发现这些职位非常有帮助:

到目前为止,我已经得到了我周围的ICommandHandler/IQueryHandler概念和依赖注入头(我使用SimpleInjector - 这真的很简单)。

的考虑方法

上述文章的方法提出使用波苏斯作为命令/查询和描述的这些调度员为以下处理程序接口的实现:

interface IQueryHandler<TQuery, TResult> 
{ 
    TResult Handle(TQuery query); 
} 

interface ICommandHandler<TCommand> 
{ 
    void Handle(TCommand command); 
} 

在MVC控制器你” d。使用本如下:

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处理程序将会惨败,但有一个例外,告诉您该方法在所有异步任务完成执行之前返回。

当然,你可以标记MVC处理程序为async这就是这个问题。

命令返回值

我想到了给定的方法,并更改了接口,以解决问题1和2中,我补充说,有一个明确的结果类型的ICommandHandler - 就像IQueryHandler。这仍然违反CQS但至少它是平原明显,这些命令将返回某种价值与不具有与结果属性杂乱的命令对象的额外好处:

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 ... 
} 

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<...> thingys到我的眼睛是很冗长,乍一看混淆,我只希望这样的事实:从一个查询/命令返回一些东西
  • 编译器警告你在同步处理程序实现中没有合适的await(我想能够编译我的ReleaseWarnings as Errors) - 你可以用一个编译指令来覆盖它...是的。 .. 好 ...
  • 我可以省略在这些情况下,async关键字使编译器高兴,但为了实现处理器接口,你就必须返回某种Task明确 - 那是相当难看
  • 我可以提供的同步和异步版本处理程序接口(或将它们全部放在一个接口中,以扩展实现),但我的理解是,理想情况下,处理程序的使用者不应该意识到命令/查询处理程序是同步的或异步的,因为这是事实一个横切关注点。如果我需要做一个以前的同步命令异步呢?我必须改变处理程序的每个消费者,这可能会破坏我的代码中的语义。
  • 在另一方面潜在 -async,处理的方法甚至会给我用我的DI容器的帮助下装饰他们

现在我不改变的同步处理是异步的能力看到一个最好的解决方案...我很茫然。

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

回答

15

异步并等待不要与传统OOP完美混合。我有一个关于这个主题的博客系列;你可能会发现post on async interfaces特别有帮助(虽然我不包括你还没有发现的任何东西)。

async的设计问题与IDisposable的设计问题非常相似;将IDisposable添加到接口是一个突破性更改,因此您需要知道是否有任何可能的实现可能是是可丢弃的(实现细节)。与async存在并行问题;您需要知道是否有任何可能的实现有史以来是异步的(一个实现细节)。

由于这些原因,我查看Task - 将接口上的方法返回为“可能是异步的”方法,就像从IDisposable继承的接口意味着它“可能拥有资源”。

我所知道的最好的办法是:

  • 定义是可能的异步与异步签名(返回Task/Task<T>)的任何方法。
  • 返回Task.FromResult(...)用于同步实现。这比没有awaitasync更合适。

这种方法几乎就是你已经在做的。对于纯粹的函数式语言,可能存在一个更理想的解决方案,但我没有看到一个适用于C#的解决方案。

+0

谢谢,概念其实非常相似......我说我个人的解决问题的办法为方便 – mfeineis

4

不是一个真正的答案,但对于它的价值,我得出了完全相同的结论和非常类似的实现。

我的ICommandHandler<T>IQueryHandler<T>分别返回TaskTask<T>。在同步实施的情况下,我使用Task.FromResult(...)。我也有一些*处理器装饰器(比如用于日志记录),你可以想象这些也需要改变。现在,我决定让'everything'潜在地等待,并且养成了与我的调度员一起使用await(在ninject内核中查找处理程序并调用它的句柄)的习惯。

我除了少数例外之外,还一直在我的webapi/mvc控制器中进行异步操作。在这些罕见的情况下,我使用Continuewith(...)Wait()以同步方法包装东西。

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

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

9

幽州:

处理程序的消费者不应该知道的事实, 命令/查询处理程序是同步或异步,因为这是一个跨领域 关注

Stephen很清楚已经触及了这一点,但异步并不是一个横切的问题(或者至少不是它在.NET中实现的方式)。异步是一个架构问题,因为您必须决定是否使用它,并且它完全有机会应用您的所有应用程序代码。它改变了你的界面,因此不可能将它隐藏起来,而不需要应用程序来了解它。

虽然.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

+0

而不是从命令处理程序 我们不能只返回TResult返回任务?正是你在文章中所做的。但支持ASYN我们可以设计命令 '公共接口SaveUserDetailCommand:ICommand的<任务> { }' 这样处理器看起来 'SaveUserDetailCommandHandler:ICommandHandler > { 公共任务手柄( SaveUserDetailCommand cmd) { } }' 从控制器我们可以做,等待mediator.execute(命令); 你认为这种方法有什么问题? – crypted

+0

@Steven我认为你对单个ICommandHandler接口是正确的。我用这种模式实现了很多功能,并且确实有很多重复的东西用于支持这两种抽象...也许我会对它做出错误的回应,并且我的命令会返回数据(并且最重要的是需要对此返回数据进行验证)是现实中的疑问。我不太确定:你会考虑转换一些东西(从数据库获取数据与用户凭据 - >保存到Excel文件流 - >下载文件)的命令或查询? thx – mfeineis

+1

@Int3ὰ:从消费角度来看,这可以很好地工作,但这种方法肯定会有问题。您仍然无法在这些处理程序周围应用泛型装饰器,因为装饰器必须知道返回类型是否为异步。试想一个简单的日志装饰器,将装饰器封装在try-finally中并记录操作的持续时间。当应用于异步处理程序时,操作的持续时间将始终接近于零,因为装饰器仅在将“ContinueWith”委托应用于返回的“任务”时才能正常工作。 – Steven

10

解释我创建了一个项目只是这一点 - 我结束了不拆分命令和查询,而不是使用请求/响应和发布/订阅 - 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类型的基类(功能伙伴单位)。这使我有一个统一的接口来发送有响应的消息,空响应是一个明确的返回值。

作为暴露命令的人,您明确选择在您的请求定义中是异步的,而不是强制每个人都是异步的。