ICommandHandler/IQueryHandler с async/wait

EDITH говорит (tl; dr)

Я пошел с вариантом предлагаемого решения; сохраняя все 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();
    }
}

Жаль, что С# не Haskell... еще 8-). Действительно пахнет как приложение Arrows. В любом случае, надеюсь, что это поможет кому угодно. Теперь к моему первоначальному вопросу: -)

Введение

Привет!

Для проекта я в настоящее время разрабатываю архитектуру приложения в С# (.NET4.5, С# 5.0, ASP.NET MVC4). С этим вопросом я надеюсь получить некоторые мнения о некоторых проблемах, которые я наткнулся на попытку включить async/await. Примечание: это довольно длинный: -)

Моя структура решения выглядит так:

  • MyCompany.Contract (Команды/Запросы и общие интерфейсы)
  • MyCompany.MyProject (Содержит бизнес-логику и обработчики команд/запросов)
  • MyCompany.MyProject.Web (веб-интерфейс MVC)

Я прочитал о поддерживаемой архитектуре и Command-Query-Separation и нашел эти сообщения очень полезными:

Пока у меня есть голова вокруг концепций ICommandHandler/IQueryHandler и зависимостей (я использую SimpleInjector - это действительно мертво просто).

Данный подход

Подход статей выше предполагает использование POCOs в качестве команд/запросов и описывает диспетчеров из них как реализации следующих интерфейсов обработчика:

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 только запросы должны возвращать значения, пока команды должны быть, а только команды. В действительности удобнее для команд возвращать значения вместо выдачи команды, а затем выполнять запрос для того, что команда должна была вернуть в первую очередь (например, идентификаторы базы данных и т.п.). Вот почему автор предложил поместить возвращаемое значение в команду POCO.
  • Не очень очевидно, что возвращается из команды, на самом деле это похоже на то, что команда является огнем и забывает тип вещи, пока вы в конце концов не столкнетесь с тем, что доступное свойство свойства доступно после выполнения обработчиком плюс команда теперь знает об этом результате
  • Обработчики должны быть синхронны для этого - запросы, а также команды. Как оказалось, с С# 5.0 вы можете вводить обработчики с питанием async/await с помощью вашего любимого контейнера DI, но компилятор не знает об этом во время компиляции, поэтому обработчик MVC терпит неудачу с исключением, говорящим вам, что метод возвращается до завершения всех асинхронных задач.

Конечно, вы можете пометить обработчик MVC как async, и об этом говорит этот вопрос.

Команды, возвращающие значения

Я подумал о данном подходе и внес изменения в интерфейсы для решения проблем 1. и 2. в этом я добавил ICommandHandler, который имеет явный тип результата - точно так же, как IQueryHandler. Это по-прежнему нарушает CQS, но, по крайней мере, очевидно, что эти команды возвращают какое-то значение с дополнительным преимуществом: не нужно загромождать объект команды с помощью свойства result:

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

Естественно, можно утверждать, что когда у вас есть тот же интерфейс для команд и запросов, зачем беспокоиться? Но я думаю, что стоит называть их по-другому - просто выглядит чище для моих глаз.

Мое предварительное решение

Затем я подумал о том, что касается 3-й проблемы... некоторые из моих обработчиков команд/запросов должны быть асинхронными (например, выдать 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 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 с Warnings as Errors) - вы можете перезаписать это с помощью прагмы... да.. Хорошо...
  • Я мог бы опустить ключевое слово async в этих случаях, чтобы сделать компилятор счастливым, но для реализации интерфейса обработчика вам нужно было бы вернуть явно какой-то Task - это довольно уродливое
  • Я мог бы предоставить синхронные и асинхронные версии интерфейсов обработчика (или поместить их все в один интерфейс, раздувая реализацию), но я понимаю, что в идеале потребитель обработчика не должен знать о том, что Обработчик команд/запросов синхронизирован или асинхронен, так как это проблема с перекрестным переключением. Что делать, если мне нужно сделать ранее синхронную команду async? Я должен был бы изменить каждого потребителя обработчика, потенциально нарушающего семантику, на моем пути через код.
  • С другой стороны, потенциально-асинхронный-обработчик-подход даже дал бы мне возможность изменять обработчики синхронизации для асинхронизации, украшая их с помощью моего контейнера DI

Сейчас я не вижу лучшего решения для этого... Я в недоумении.

Любой, у кого есть аналогичная проблема и изящное решение, о котором я не думал?

Ответ 1

Async и ожидание не сочетаются с традиционным ООП. У меня есть серия блога на эту тему; вы можете найти сообщение на асинхронных интерфейсах, в частности полезное (хотя я не покрываю ничего, что вы еще не обнаружили).

Проблемы проектирования вокруг async очень похожи на проблемы вокруг IDisposable; это перерыв для добавления IDisposable к интерфейсу, поэтому вам нужно знать, может ли любая возможная реализация когда-либо быть одноразовой (деталь реализации). Параллельная проблема существует с async; вам нужно знать, может ли любая возможная реализация когда-либо быть асинхронной (деталь реализации).

По этим причинам я рассматриваю методы Task -returning на интерфейсе как "возможно асинхронные" методы, так же как интерфейс, наследующий от IDisposable, означает, что он "возможно, владеет ресурсами".

Лучший подход, о котором я знаю, - это:

  • Определите любые асинхронные методы с асинхронной подписью (возврат Task/Task<T>).
  • Возвращает Task.FromResult(...) для синхронных реализаций. Это более верно, чем async без await.

Этот подход - это почти то, что вы уже делаете. Более идеальное решение может существовать для чисто функционального языка, но я не вижу его для С#.

Ответ 2

Я создал проект только для этого - я не разбил команды и запросы, вместо этого использовал запрос/ответ и 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 (Unit для функциональных пользователей). Это позволило мне иметь единый интерфейс для отправки сообщений с ответами, причем нулевой ответ был явным возвращаемым значением.

Как кто-то, выставляющий команду, вы явно отказываетесь быть асинхронным в своем определении запроса, а не заставляете всех быть асинхронными.

Ответ 3

Вы заявляете:

потребитель обработчика не должен знать о том, что Обработчик команд/запросов синхронизируется или асинхронно, поскольку это перекрестная резка беспокойство

Стивен. Очевидно, уже немного коснулся этого, но async не является сквозной проблемой (или, по крайней мере, не так, как она реализована в .NET). Async - это архитектурная проблема, так как вам нужно решить, использовать его или нет, и это полностью соответствует всему вашему коду приложения. Это изменяет ваши интерфейсы, и поэтому невозможно "прокрасть" это, без приложения, чтобы знать об этом.

Хотя .NET упростил async, как вы сказали, он все еще болит вашими глазами и умом. Возможно, он просто нуждается в умственном обучении, но мне действительно интересно, стоит ли вообще переходить на асинхронный режим для большинства приложений.

В любом случае, предотвратите наличие двух интерфейсов для обработчиков команд. Вы должны выбрать один из них, потому что наличие двух отдельных интерфейсов заставит вас дублировать все ваши декораторы, которые вы хотите применить к ним, и дублирует ваше доверительное управление 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> и ограничения общего типа вам не будет предоставлена ​​поддержка времени компиляции. Это я объяснил в Между тем... на стороне запроса моей архитектуры

Ответ 4

Не совсем ответ, но для чего он стоит, я пришел к тем же самым выводам и очень похожей реализации.

Мои ICommandHandler<T> и IQueryHandler<T> возвращают Task и Task<T> соответственно. В случае синхронной реализации я использую Task.FromResult(...). У меня также были некоторые обработчики обработчиков на месте (например, для регистрации), и, как вы можете себе представить, их также нужно было изменить.

В настоящее время я решил сделать "все" потенциально ожидаемым и привык использовать await в сочетании с моим диспетчером (обнаруживает обработчик в ядре ninject и вызывает обращение к нему).

Я пошел асинхронно, также в моих контроллерах webapi/mvc, за некоторыми исключениями. В тех редких случаях я использую Continuewith(...) и Wait() для обертывания вещей синхронным способом.

Другим, связанным с этим разочарованием является то, что MR рекомендует называть методы с суффиксом * Async в случае, если thay являются (duh) асинхронными. Но поскольку это решение реализации я (на данный момент) решило придерживаться Handle(...), а не HandleAsync(...).

Это определенно не удовлетворительный результат, и я также ищу лучшее решение.