ICommandHandler / IQueryHandler с асинхронным вызовом / ожиданием

ЭДИТ говорит (TL; доктор)

Я пошел с вариантом предложенного решения; сохраняя всеICommandHandlerс иIQueryHandlerпотенциально асинхронный и возвращающий решенную задачу в синхронных случаях. Тем не менее, я нене хочу использоватьTask.FromResult(...) повсюду, поэтому для удобства я определил метод расширения:

public static class TaskExtensions
{
    public static Task AsTaskResult(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
{
    public Task Handle(MyQuery query)
    {
        return true.AsTaskResult();
    }
}

class MyAsynchronousQueryHandler : IQueryHandler
{
    public async Task Handle(MyQuery query)
    {
        return await this.callAWebserviceToReturnTheResult();
    }
}

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

Вступление

Привет!

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

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

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

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

Между тем на стороне запроса моей архитектурыМежду тем на командной стороне моей архитектурыНаписание очень удобных в обслуживании сервисов WCF

Пока яу меня есть голова вокруг /ICommandHandlerIQueryHandler внедрение концепций и зависимостей (I 'м используюSimpleInjector - Это'очень просто).

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

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

interface IQueryHandler
{
    TResult Handle(TQuery query);
}

interface ICommandHandler
{
    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 authenticateUser;

    public AuthenticateController(ICommandHandler 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
    }
}

Некоторые из моих наблюдений относительно этого подхода:

В чистом видеОКК только запросы должны возвращать значения, в то время как команды должны быть, ну только команды. В действительности, командам удобнее возвращать значения, а не выдавать команду, а затем делать запрос о том, что команда должна была вернуть в первую очередь (например, идентификаторы базы данных или тому подобное). Тот'почемуавтор предложил положить возвращаемое значение в команду POCO.Не очень очевидно, что возвращается из команды, на самом деле этовыглядит например, команда - это строчка типа «забывай и забывай», пока в конце концов вы не столкнетесь со странным свойством результата, к которому обращаются после запуска обработчика, плюс команда теперь знает об этом »результатОбработчикииметь быть синхронным, чтобы это работало - запросы и команды. Как выясняется с C # 5.0 вы можете ввестиasync/await обработчики с помощью вашего любимого DI-контейнера, но компилятор неЯ не знаю об этом во время компиляции, поэтому обработчик MVC с треском провалится, за исключением того, что метод возвращен до завершения выполнения всех асинхронных задач.

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

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

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

interface ICommandHandler
{
    TResult Handle(TCommand command);
}

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

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

Затем я подумал о третьей проблеме под рукой ... некоторые из моих обработчиков команд / запросов должны быть асинхронными (например, выдачаWebRequest в другой веб-сервис для аутентификации) другие нет. Так что я подумал, что было бы лучше спроектировать мои обработчики с нуля дляasync/await - что, конечно, всплывает в обработчики MVC даже для обработчиков, которые на самом деле являются синхронными:

interface IQueryHandler
{
    Task Handle(TQuery query);
}

interface ICommandHandler
{
    Task Handle(TCommand command);
}

interface ICommandHandler
{
    Task 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 authenticateUser;

    public AuthenticateController(ICommandHandler authenticateUser) 
    {
        // Injected via DI container
        this.authenticateUser = authenticateUser;
    }

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

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

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