I've been looking into CQRS/MediatR lately. But the more I drill down the less I like it. Perhaps I've misunderstood something/everything.
So it starts out awesome by claiming to reducing your controller to this
public async Task<ActionResult> Edit(Edit.Query query)
{
var model = await _mediator.SendAsync(query);
return View(model);
}
Which fits perfectly with the thin controller guideline. However it leaves out some pretty important details - error handling.
Lets look at the default Login
action from a new MVC project
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation(1, "User logged in.");
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning(2, "User account locked out.");
return View("Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
Converting that presents us with a bunch of real world problems. Remember the goal is to reduce it to
public async Task<IActionResult> Login(Login.Command command, string returnUrl = null)
{
var model = await _mediator.SendAsync(command);
return View(model);
}
One possible solution to this is to return an CommandResult<T>
instead of a model
and then handle the CommandResult
in a post action filter. As discussed here.
One implementation of the CommandResult
could be like this
public interface ICommandResult
{
bool IsSuccess { get; }
bool IsFailure { get; }
object Result { get; set; }
}
However that doesn't really solve our problem in the Login
action, because there are multiple failure states. We could add these extra failure states to ICommandResult
but that is a great start for a very bloated class/interface. One might say it doesn't comply with Single Responsibility (SRP).
Another problem is the returnUrl
. We have this return RedirectToLocal(returnUrl);
piece of code. Somehow we need to handle conditional arguments based on the success state of the command. While I think that could be done (I'm not sure if the ModelBinder can map FromBody and FromQuery (returnUrl
is FromQuery) arguments to a single model). One can only wonder what kind of crazy scenarios could come down the road.
Model validation have also become more complex along with returning error messages. Take this as an example
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
We attach an error message along with the model. This sort of thing cannot be done using an Exception
strategy (as suggested here) because we need the model. Perhaps you can get the model from the Request
but it would be a very involved process.
So all in all I'm having a hard time converting this "simple" action.
I'm looking for inputs. Am I totally in the wrong here?