Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC5 Async ActionResult. Is that possible?

In our application we have CQRS: we have IAsyncCommand with IAsyncCommandHandler<IAsyncCommand>.

Usually the command is processed via Mediator like this:

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); 

That works fine. Now I noticed that I do a lot of repetitive code in controller actions:

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());
}

And this patterns repeats over and over in many-many controllers. So for single-threaded commands I've done simplification:

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
}

And after some plumbing done, my controller action looks like this:

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

This works well for sync-commands where I don't need to do async-await patterns. As soon as I try to do async operations, this does not compile, as there is no AsyncActionResult in MVC (or there is and I can't find it) and I can't make MVC framework use async operations on void ExecuteResult(ControllerContext context).

So, any ideas how I can make a generic implementation of the controller action I quoted on top of the question?

like image 608
trailmax Avatar asked Dec 03 '14 12:12

trailmax


1 Answers

Your solution seems overly complex, highly smelly (contains both service location, and other smells) and seems to miss the point of what ActionResults are (command objects themselves, really).

In reality, this is a good example of The XY Problem. Rather than asking about your actual problem, which is refactoring common code in your action methods in an async friendly way, you have instead come up with an overly complex solution that you think solves your problem. Unfortunately, you can't figure out how to make it work, so you ask about THAT problem rather than your real problem.

You can achieve what you want with a simple helper function. Something like this:

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
    }
}

Alternatively, you could make it a stateful object and inject the mediator through cascaded dependencies via constructor injection. It's not easy to inject ModelState, unfortunately, so that still needs to be passed as a parameter to the method.

You could also just pass the string for the ActionResults, but since there's no RedirectToActionResult object to new up, you'd have to mess with initializing a RedirectToRoute object and it's just easier to pass the ActionResult. It's also much easier to use the Controllers View() function than to construct a new ViewResult yourself.

You could also use the Func<ActionResult> approach that Sambo uses, which makes it lazy evaluate, so it only calls the RedirectToAction method when necessary. I don't think RedirectToAction has enough overhead to make it worth it.

like image 93
Erik Funkenbusch Avatar answered Oct 10 '22 14:10

Erik Funkenbusch