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?