ASP.NET – Is CQRS/MediatR Worth It for Development?

architectural-patternsasp.netc

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

source

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?

Best Answer

I think you're expecting too much of the pattern you're using. CQRS is specifically designed to address the difference in model between query and commands to the database, and MediatR is just in-process messaging library. CQRS doesn't claim to eliminate the need for business logic like you're expecting them to. CQRS is a pattern for data access, but your problems are with presentation layer--redirects, views, controllers.

I think you may be mis-applying the CQRS pattern to authentication. With login it cannot be a modelled as a command in CQRS because

Commands: Change the state of a system but do not return a value
- Martin Fowler CommandQuerySeparation

In my opinion authentication is a poor domain for CQRS. With authentication you need strongly consistent, synchronous request-response flow so you can 1. check user's credentials 2. create a session for the user 3. handle any of the variety of edge cases that you've identified 4. immediately grant or deny user in response.

Is CQRS/MediatR worth it when developing an ASP.NET application?

CQRS is a pattern that has very specific uses. It's purpose is to model queries and commands instead of having a model for records as used in CRUD. As systems become more complex, the demands of views are often more complex than just showing a single record or a handful of records, and a query can better model the needs of the application. Similarly commands can represent changes to many records instead of CRUD which you change single records. Martin Fowler warns

Like any pattern, CQRS is useful in some places, but not in others. Many systems do fit a CRUD mental model, and so should be done in that style. CQRS is a significant mental leap for all concerned, so shouldn't be tackled unless the benefit is worth the jump. While I have come across successful uses of CQRS, so far the majority of cases I've run into have not been so good, with CQRS seen as a significant force for getting a software system into serious difficulties.
- Martin Fowler CQRS

So to answer your question CQRS should not be the first resort when designing an application when CRUD is suitable. Nothing in your question gave me the indication that you have a reason to use CQRS.

As for MediatR, it's an in-process messaging library, it aims to decouple requests from request handling. You must again decide if it will improve your design to use this library. I'm personally not an advocate of in-process messaging. Loose-coupling can be achieved in simpler ways than messaging, and I would recommend you start there.

Related Topic