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 и нашел эти сообщения очень полезными:
- Между тем на стороне запроса моей архитектуры
- Между тем на командной стороне моей архитектуры
- Написание высокоподдерживаемых служб WCF
Пока у меня есть голова вокруг концепций 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
Сейчас я не вижу лучшего решения для этого... Я в недоумении.
Любой, у кого есть аналогичная проблема и изящное решение, о котором я не думал?