2014-12-03 2 views
6

В нашем приложении у нас есть CQRS: у нас есть IAsyncCommand с IAsyncCommandHandler<IAsyncCommand>.MVC5 Async ActionResult. Это возможно?

Обычно команда обрабатывается через Посредника, как это:

var mediator = //get mediator injected into MVC controller via constructor 
var asyncCommand = // construct AsyncCommand 
// mediator runs ICommandValidator and that returns a list of errors if any 
var errors = await mediator.ProcessCommand(asyncCommand); 

Это работает отлично. Теперь я заметил, что я делаю много повторяющегося код в контроллере действий:

public async virtual Task<ActionResult> DoStuff(DoStuffAsyncCommand command) 
{ 
    if (!ModelState.IsValid) 
    { 
     return View(command); 
    } 

    var result = await mediator.ProcessCommandAsync(command); 

    if (!result.IsSuccess()) 
    { 
     AddErrorsToModelState(result); 
     return View(command); 
    } 
    return RedirectToAction(MVC.HomePage.Index()); 
} 

И это модель повторяется снова и снова во многих-многих контроллерах. Таким образом, для однопоточных команд я сделал упрощение:

public class ProcessCommandResult<T> : ActionResult where T : ICommand 
{ 
    private readonly T command; 
    private readonly ActionResult failure; 
    private readonly ActionResult success; 
    private readonly IMediator mediator; 


    public ProcessCommandResult(T command, ActionResult failure, ActionResult success) 
    { 
     this.command = command; 
     this.success = success; 
     this.failure = failure; 

     mediator = DependencyResolver.Current.GetService<IMediator>(); 
    } 

    public override void ExecuteResult(ControllerContext context) 
    { 
     if (!context.Controller.ViewData.ModelState.IsValid) 
     { 
      failure.ExecuteResult(context); 
      return; 
     } 

     var handlingResult = mediator.ProcessCommand(command); 

     if (handlingResult.ConainsErrors()) 
     { 
      AddErrorsToModelState(handlingResult); 
      failure.ExecuteResult(context); 
     } 

     success.ExecuteResult(context); 
    } 
    // plumbing code 
} 

И после того, как некоторые сантехнические сделано, мое действие контроллер выглядит следующим образом:

public virtual ActionResult Create(DoStuffCommand command) 
{ 
    return ProcessCommand(command, View(command), RedirectToAction(MVC.HomePage.Index())); 
} 

Это хорошо работает для синхронизации команд, где я не» t необходимо сделать async-await узоров. Как только я попытаюсь выполнить операции async, это не скомпилируется, так как в MVC нет AsyncActionResult (или есть, и я не могу его найти), и я не могу сделать структуру MVC для использования асинхронных операций на void ExecuteResult(ControllerContext context).

Итак, любые идеи, как я могу сделать общую реализацию действия контроллера, которую я цитировал в верхней части вопроса?

+5

Я не вижу, где участвует что-то под названием «AsyncActionResult». Просто верните задачу или задачу , если вы реализуете общий метод. Асинхронные действия всегда возвращают задачи. 'async void' - очень специфический синтаксис, используемый только для асинхронных обработчиков событий (или обработчиков подобных методов) и * нигде * else. Асинхронный эквивалент метода 'void' - это функция, возвращающая' Task'. Эквивалентом функции является функция, возвращающая 'Task ' –

+0

@PanagiotisKanavos да, я вполне понимаю, что я не должен использовать 'async void'. И я не могу просто вернуть Task , потому что мне нужно проверить, находится ли «ModelState» в допустимом состоянии до того, как я запускаю посредник, и это связано с тем, что вы проходите через конвейер MVC и каким-то образом вытаскиваете «ModelState» из структуры. – trailmax

+0

Кажется, что вы смешиваете различные проблемы, такие как входящий запрос с самим действием * и * ожидаемый результат. На данный момент ваш класс ProcessCommandResult выглядит как контроллер. Если вы хотите переопределить проверку, привязку и т. Д., В MVC есть другие механизмы. Фактически, то, что у вас здесь, нарушает CQRS - вы используете ответ (ActionResult), как если бы это была сама команда, реализуя ICommand. –

ответ

0

Похоже, что действие по-прежнему является лучшим местом для обработки вашей логики вместо использования ActionResult.

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

public class BaseCommandController : Controller 
{ 
    protected IMediator Mediator { get { return DependencyResolver.Current.GetService(typeof (IMediator)) as IMediator; } } 

    public async virtual Task<ActionResult> BaseDoStuff<TCommand>(TCommand command, Func<ActionResult> success, Func<ActionResult> failure) 
    { 
     if (!ModelState.IsValid) 
     { 
      return failure(); 
     } 

     var result = await Mediator.ProcessCommand(command); 

     if (!result.IsSuccess()) 
     { 
      AddErrorsToModelState(result); 
      return failure(); 
     } 

     return success(); 
    } 

    private void AddErrorsToModelState(IResponse result) 
    { 
    } 

} 

действия Вашего контроллера затем отображаются как ...

public class DefaultController : BaseCommandController 
{ 
    protected async virtual Task<ActionResult> DoStuff(DoStuffAsyncCommand command) 
    { 
     return await BaseDoStuff(command,() => RedirectToAction("Index"),() => View(command)); 
    } 
} 
+0

У нас было именно это, и это не сработает для нас по разным причинам. – trailmax

1

Ваше решение кажется слишком сложным, весьма вонючий (содержит как сервисный центр, и другие запахи) и, кажется, упускают суть того, что ActionResults (сами объекты команд, действительно).

В действительности, это хороший пример The XY Problem. Вместо того, чтобы спрашивать о вашей реальной проблеме, которая реорганизует общий код в ваших методах действий с помощью асинхронного подхода, вы вместо этого придумали слишком сложное решение, которое, по вашему мнению, решит вашу проблему. К сожалению, вы не можете понять, как заставить его работать, поэтому вы спрашиваете об этой проблеме, а не о своей реальной проблеме.

Вы можете достичь того, чего хотите, с помощью простой вспомогательной функции. Что-то вроде этого:

public async virtual Task<ActionResult> DoStuff(DoStuffAsyncCommand command) 
{ 
    return await ControllerHelper.Helper(command, ModelState, _mediator, 
     RedirectToAction(MVC.HomePage.Index()), View(command), View(command)); 
} 

public static class ControllerHelper 
{ 
    // You may need to constrain this to where T : class, didn't test it 
    public static async Task<ActionResult> Helper<T>(T command, 
     ModelStateDictionary ModelState, IMediator mediator, ActionResult returnAction, 
     ActionResult successAction, ActionResult failureAction) 
    { 
     if (!ModelState.IsValid) 
     { 
      return failureResult; 
     } 

     var result = await mediator.ProcessCommandAsync(command); 

     if (!result.IsSuccess()) 
     { 
      ModelState.AddErrorsToModelState(result); 
      return successResult; 
     } 

     return returnAction; 
    } 

    public static void AddErrorsToModelState(this ModelStateDictionary ModelState, ...) 
    { 
     // add your errors to the ModelState 
    } 
} 

В качестве альтернативы, вы можете сделать это с состоянием объекта и ввести посредника через каскадно зависимостей с помощью инъекции конструктора. К сожалению, нелегко ввести ModelState, так что все еще нужно передать его в качестве параметра для метода.

Вы также можете просто передать строку для ActionResults, но поскольку объект RedirectToActionResult отсутствует, вам придется запутаться с инициализацией объекта RedirectToRoute, и это просто проще передать ActionResult. Также гораздо проще использовать функцию Controllers View(), чем самостоятельно создавать новый ViewResult.

Вы также можете использовать подход Func<ActionResult>, который использует самбо, что делает его ленивым, поэтому он вызывает только метод RedirectToAction, когда это необходимо. Я не думаю, что для RedirectToAction достаточно накладных расходов, чтобы это стоило того.

+0

Это почти идентично тому, что я закончил с собой после достаточного количества итераций рефакторинга. – trailmax

+1

@trailmax - да, я не заметил, что вопрос был несколько месяцев назад. На него наткнулся самбо. Рад, что вы пришли к такому же выводу ... Мне не нравятся базовые контроллеры. –