2014-01-08 3 views
14

EDITH говорит (Tl; др)

Я пошел с вариантом предлагаемого решения; сохраняя все ICommandHandler и IQueryHandler s потенциально асинхронными и возвращающими разрешенную задачу в синхронных случаях. Тем не менее, я не хочу использовать 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)

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

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

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

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

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

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

Команда Возвращающихся Значения

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

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

Прямо сейчас я не Лучшее решение ... Я в недоумении.

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

ответ

15

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

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

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

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

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

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

+0

Спасибо, понятия на самом деле очень похожи ... Я добавил свое личное решение вопроса для удобства. – mfeineis

4

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

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

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

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

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

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

9

Вы состояние:

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

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

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

В любом случае, предотвратите наличие двух интерфейсов для обработчиков команд. Вы должны выбрать один из них, потому что наличие двух отдельных интерфейсов заставит вас дублировать все ваши декораторы, которые вы хотите применить к ним, и дублирует ваше доверительное управление DI. Таким образом, либо есть интерфейс, который возвращает Task и использует свойства вывода, либо переходите с Task<TResut> и возвращаете какой-то тип Void в случае, если нет возврата типа.

Как вы можете себе представить (статьи, на которые вы указываете мои), мое личное предпочтение состоит в том, чтобы иметь метод или 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

+0

Вместо того, чтобы возвращать задачу из обработчика команд мы не можем просто вернуть TResult? именно то, что вы сделали в своей статье. Но для поддержки ASYN мы можем конструировать команды, как 'интерфейс SaveUserDetailCommand общественности: ICommand <Задача > { }' так обработчик выглядит 'SaveUserDetailCommandHandler: ICommandHandler > { общественности Задача Handle (SaveUserDetailCommand cmd) { } } ' от контроллера, с которым мы можем тогда работать, ждать mediator.execute (команда); Считаете ли вы, что любая проблема с этим подходом? – crypted

+0

@Steven Я думаю, что вы правы с единственным интерфейсом «ICommandHandler». Я реализовал совсем немного с этим шаблоном, и на самом деле есть много дублированных вещей для поддержки абстракций ... может быть, я ошибаюсь, и мои команды возвращают данные (и, кроме того, для подтверждения этих данных требуется проверка) - это запросы в реальности. Я не уверен в этом: подумаете ли вы о преобразовании чего-либо (выборка данных из БД с учетными данными пользователя -> Сохранить в файл Excel-Filestream-> Загрузить) - команда или запрос? thx – mfeineis

+1

@ Int3ὰ: С точки зрения потребления, это будет работать нормально, но, безусловно, будут проблемы с этим подходом. Вы по-прежнему не можете применять общий декоратор вокруг этих обработчиков, потому что декоратор должен знать, является ли тип возврата асинхронным или нет. Представьте себе простой декоратор оформления, который обертывает декоратор в try-finally и регистрирует продолжительность операции. Когда применяется к обработчику async, продолжительность операции всегда будет близка к нулю, поскольку декоратор будет работать только корректно, когда он применяет делегат '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 (Unit для функциональных людей). Это позволило мне иметь единый интерфейс для отправки сообщений с ответами, а нулевой ответ - явное возвращаемое значение.

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

Смежные вопросы