ЭДИТ говорит (tl;dr)
Я пошел с вариантом предложенного решения; сохраняя все ICommandHandler
s и IQueryHandler
s потенциально синхронными и возвращая решенную задачу в синхронных случаях. Тем не менее, я не хочу использовать 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();
}
}
Жаль, что C# не Haskell... пока 8-). Действительно пахнет приложением Arrows. В любом случае, надеюсь, что это кому-нибудь поможет. Теперь к моему первоначальному вопросу :-)
Вступление
Привет!
Для проекта я в настоящее время разрабатываю архитектуру приложения на С# (.NET4.5, С# 5.0, ASP.NET MVC4). С этим вопросом я надеюсь получить некоторые мнения о некоторых проблемах, с которыми я столкнулся, пытаясь включить async/await
. Примечание: это довольно длинное сообщение :-)
Структура моего решения выглядит так:
MyCompany.Contract
(Команды/Запросы и общие интерфейсы)MyCompany.MyProject
(содержит бизнес-логику и обработчики команд/запросов)MyCompany.MyProject.Web
(веб-интерфейс MVC)
Я прочитал об обслуживаемой архитектуре и разделении команд-запросов и нашел эти сообщения очень полезными:
- Тем временем на стороне запросов моей архитектуры
- Тем временем на командной стороне моей архитектуры
- Написание легко поддерживаемых сервисов WCF
До сих пор я разбирался в концепциях ICommandHandler
/IQueryHandler
и внедрении зависимостей (я использую SimpleInjector - он действительно мертв просто).
Данный подход
Подход, описанный в приведенных выше статьях, предлагает использовать POCO в качестве команд/запросов и описывает их диспетчеры как реализации следующих интерфейсов обработчиков:
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 только запросы должны возвращать значения, а команды должны быть, ну только команды. На самом деле командам удобнее возвращать значения вместо того, чтобы выполнять команду, а затем выполнять запрос для того, что команда должна была вернуть в первую очередь (например, идентификаторы базы данных или тому подобное). Вот почему автор предложил помещать возвращаемое значение в команда ПОКО.
- Не очень очевидно, что возвращается из команды, на самом деле это выглядит так, как будто команда представляет собой нечто типа «огонь и забудь», пока вы в конечном итоге не столкнетесь со странным свойством результата, доступ к которому осуществляется после запуска обработчика плюс команда теперь знает о своем результате
- Чтобы это работало, обработчики должны быть синхронными — как запросы, так и команды. Как оказалось, в C# 5.0 вы можете внедрить обработчики с поддержкой
async/await
с помощью вашего любимого DI-контейнера, но компилятор не знает об этом во время компиляции, поэтому обработчик MVC с треском провалится с исключением, сообщающим вам, что метод возвращается до завершения выполнения всех асинхронных задач.
Конечно, вы можете пометить обработчик MVC как async
, и именно об этом этот вопрос.
Команды, возвращающие значения
Я обдумал данный подход и внес изменения в интерфейсы для решения проблем 1 и 2, добавив ICommandHandler
с явным типом результата - точно так же, как IQueryHandler
. Это по-прежнему нарушает CQS, но, по крайней мере, очевидно, что эти команды возвращают какое-то значение с дополнительным преимуществом, заключающимся в том, что не нужно загромождать объект команды свойством результата:
interface ICommandHandler<TCommand, TResult>
{
TResult Handle(TCommand command);
}
Естественно, кто-то может возразить, что если у вас одинаковый интерфейс для команд и запросов, зачем беспокоиться? Но я думаю, стоит назвать их по-другому - просто чище на мой взгляд.
Мое предварительное решение
Затем я серьезно задумался над третьей проблемой... некоторые из моих обработчиков команд/запросов должны быть асинхронными (например, выдача 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 ...
}
Контроллер аутентификации:
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<...>
на мой взгляд очень многословны и на первый взгляд скрывают тот факт, что я хочу вернуть что-то только из запроса/команды. - Компилятор предупреждает вас об отсутствии соответствующего
await
в реализациях синхронного обработчика (я хочу иметь возможность компилировать свойRelease
сWarnings as Errors
) - вы можете перезаписать это с помощью прагмы... да... ну... - Я мог бы опустить ключевое слово
async
в этих случаях, чтобы сделать компилятор счастливым, но для реализации интерфейса обработчика вам пришлось бы явно возвращать какое-тоTask
- это довольно некрасиво. - Я мог бы предоставить синхронную и асинхронную версии интерфейсов обработчика (или поместить их все в один интерфейс, раздувая реализацию), но я понимаю, что в идеале потребитель обработчика не должен знать о том, что команда/запрос обработчик является синхронным или асинхронным, поскольку это сквозная проблема. Что, если мне нужно сделать ранее синхронную команду асинхронной? Мне пришлось бы изменить каждого потребителя обработчика, потенциально нарушая семантику на моем пути через код.
- С другой стороны, потенциально-async-handlers-подход даже дал бы мне возможность изменить обработчики синхронизации на асинхронные, украсив их с помощью моего контейнера внедрения зависимостей.
Сейчас я не вижу лучшего решения для этого... Я в растерянности.
У кого-нибудь есть похожая проблема и элегантное решение, о котором я не подумал?