MVC5 Async ActionResult. Is it possible?

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

Typically, a command is processed through an intermediary as follows:

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

It works great. Now I noticed that I am doing a lot of repeating 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 these patterns are repeated over and over in many controllers. Therefore, for single-threaded commands I made a 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 performing any plumbing, the action of my controller is as follows:

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

This works well for synchronization commands, where I don't need to do async-await templates. As soon as I try to perform async operations, this will not compile, since MVC does not have AsyncActionResult (or is, and I cannot find it), and I cannot force the MVC structure to use async operations for void ExecuteResult(ControllerContext context) .

So, any ideas how I can make a general implementation of the controller action that I quoted at the top of the question?

+6
source share
2 answers

Your solution seems overly complex, very smelly (it contains both the location of the service and other odors), and it seems that it does not understand what ActionResults (command objects themselves) are.

In fact, this is a good example of Problem XY . Instead of asking about your real problem, which will reorganize the common code in your action methods using an asynchronous approach, you instead come up with an overly complex solution that, in your opinion, will solve your problem. Unfortunately, you cannot figure out how to make it work, so you are asking about this problem and not about your real problem.

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

 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 can create an object with state preservation and insert a mediator through cascading dependencies through constructor injection. Unfortunately, it is not easy to enter ModelState, so you still need to pass it as a parameter to the method.

You can also just pass a string for ActionResults, but since there is no RedirectToActionResult object for the new one, you will have to get confused with the initialization of the RedirectToRoute object, and it is just easier to pass an ActionResult to it. It is also much easier to use the Controller View () function than to create a new ViewResult yourself.

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

+1
source

Action seems to be the best place to process your logic instead of using ActionResult.

If the code is duplicated, why not use a base class with a protected helper method ...?

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

Your controller actions are then displayed as ...

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

Source: https://habr.com/ru/post/979047/


All Articles