5

We recently embraced Clean Architecture and CQRS, by way of Mediatr, in several of our new applications. A similar (SPA-based) demo can be seen here. For many of our WebApi-based projects, this worked wonderfully. The presence of pipeline behaviors made it incredibly easy to centralize our validation, logging, authentication, and other shared request-based logic. However, after attempting to use the paradigm in MVC, we appear to have hit a small roadblock.

Much like our WebApi-based projects, many of the interactions can be easily modeled using a single request/handler, but there are quite a few that cannot. One such example are Comments in our system. Comments are independent entities that are often manipulated outside of our other constructs. However, in some cases, such as in our Jobs entities, we will need to manipulate comments at the same time as our main Job entity.

In theory, I could build services in my presentation project that pick apart the MVC viewmodels and call the appropriate commands. This approach has some drawbacks, in many cases, it would cause my Mediatr pipeline to execute several times as each user request was executed. The consequences of this are mostly trivial, but it removes my ability to perform Entity Framework updates as a single transaction.

Alternatively, I can make more requests to handle the various different user interactions with the system, but this requires me to extract my shared logic into services. This is the approach that I've mostly been leaning towards, but it also comes with its own share of problems. First, my IRequests become significantly more complex and bloated. If I'm housing various different entity types in each request, I have to build special validators for all of them. It also means the design of my backend system is largely driven by my UI (which I'm trying to avoid).

The other complication with moving my shared logic into dedicated services is the validation piece. Every request that comes in on my mediatr pipeline is validated for correctness. If there are values provided, they are confirmed to be proper. If other entities are referenced, they're looked up in the database to ensure they exist. There are various other checks, but this is just a bit of it.

When moving my logic to shared services, traditionally, I'd be inclined to include that same level of error checking and validation in the services themselves, but this leads to a lot of duplicity and more overhead.

What are some strategies that allow me to continue to adhere to Clean Architecture's layer separation while not duplicating or losing out on the benefits I gain from Mediatr's pipelines?

1 Answer 1

6

For context, I'm working in a similar architecture to yours, and recently I spent a few days investigating how to implement reusable logic that gets reused across commands - so I think we're looking at the exact same issue here.

My intuitive approach was to daisy-chain Mediatr requests, but I was advised to not do so. The reasons for not doing so aren't 100% clear to me, but several people independently indicated that it leads to maintenance issues down the line, so I steered clear.

The predominant suggestion I received, was to abstract that reusable logic into service, but a different kind of service than you're describing.

In theory, I could build services in my presentation project that pick apart the MVC viewmodels and call the appropriate commands.

You seem to put the services above the commands, i.e. Controller > Service > Command > Persistence. However, the advice I was given (and have implemented successfully) puts the services below the commands, i.e. Controller > Command > Service > Persistence. The services would effectively become domain (or application) services.

Inverting the hierarchy of your commands and services actually solves several of the issues you highlight:

it would cause my Mediatr pipeline to execute several times as each user request was executed

You'd have one request which could speak to multiple services during its handling. This means that the pipeline is only being executed once.

When moving my logic to shared services, traditionally, I'd be inclined to include that same level of error checking and validation in the services themselves, but this leads to a lot of duplicity and more overhead.

Putting Mediatr requests above your services, a service would be shielded behind a Mediatr request. So if your Mediatr requests are being validated, you can assure yourself that only validated content will be passed from your Mediatr request to your service.

If your top-level application (e.g. api controller) can only interact with your domain via Mediatr requests; and those requests are being validated, then you've covered your bases in terms of data validation and don't need to re-validate it inside of the Mediatr requests and its dependencies.

The consequences of this are mostly trivial, but it removes my ability to perform Entity Framework updates as a single transaction.

How you scope your db context is very much up to you. I don't quite see a problem emerging here; but rather that you need to evaluate how to handle your context's lifetime based on how you structure your transactional logic (i.e. the logic that should be committed as a single transaction).

Personally, in a web context, I tend to scope my db contexts to a single web request. It keeps things simple and generally suffices for most use cases.
However, you could also scope your db context to a specific Mediatr request, i.e. if a single web requests generates two Mediatr requests, you generate a new db context for each request.

In either case, both the command handler and its dependencies (e.g. your comment service) share the same context, which means you can still commit your logic as a single transaction.

If I'm housing various different entity types in each request, I have to build special validators for all of them.

You didn't elaborate on how your validation works, so I can't address that part. But I would assume that however you're currently working, you should be able to promote reuse of validation logic where necessary.

E.g. if both Job and Task have comments, and the validation rules for comments are always the same (e.g. message must not be empty), then you need to abstract your comment validation logic in a way that both the Job and Task Mediatr request validation can depend on it.

This is essentially the same as with your Mediatr requests/services. There, you abstract the reusable comment logic into a service so multiple Mediatr request handlers can depends on that same logic. Here, you abstract the comment validation logic into its own separate class, so that multiple Mediatr request validators can depend on that same validation logic.

In my experience, services with reusable logic tend to then also necessitate reusable validation logic for their input. I can't really think of an example where that has not been the case for me, historically.

On a personal note, I find that the FluentValidation library makes it relatively easy to delegate part of your validation logic to different "validator services" (for lack of a better name).

It also means the design of my backend system is largely driven by my UI

This may be a pedantic rephrasing, but I find it a meaningful shift of approach. I think you're conflating "UI driven" and "command driven".

Essentially, you define the commands that your application needs. The backend then makes sure that these commands are implemented, and the UI makes sure that the user is able to issue these commands when needed.

When done right, this means that both the backend and your UI follow the same structure, but that does not mean that your backend is UI driven. Rather, both your backend and UI are command-driven, which is perfectly fine.

8
  • Thanks for the input! For what it's worth, the approach you've described in your post is nearly verbatim what I've done so far. I'm using FluentValidation to handle my validation logic, and my reused validators are abstracted into their own services. The shared command logic is also placed into shared services on the application side. My only reason for suggesting the inverse was to simplify the interactions with my backend system by making my commands and queries smaller and more targeted.
    – JD Davis
    Oct 27, 2020 at 14:38
  • 1
    @JDDavis: As much as I understand the intention, I don't think it simplifies the interactions, but rather subdivides them, making it harder to track the atomicity of the command itself. Since the command is essentially its own scope, it's significantly easier to put the command at the top, and let the (shared) scope extend into its dependency graph.
    – Flater
    Oct 27, 2020 at 14:51
  • I understand, but surely there must be some sort of happy medium. Having dozens of commands that model different (but similar or interconnected) interactions seems a bit cluttered. Realistically, it's just a few extra files, especially if all the shared logic is abstracted away in some other series of services. However, some sort of Saga construct feels like it could add much more to the design workflow.
    – JD Davis
    Oct 27, 2020 at 15:36
  • @Flater jumping a bit late to the party but i'm intrigued by your answer and wondering about one thing: how do you handle situation if your client needs to create some resource and it expects created value (not reference) in response? In that case you end up either having your command handler to return value (which is not good) or you need to do that logic in controller (which then defeats one of the purposes of Mediatr => clean/thin controller actions). How do you address these issues? thanks!
    – tlzg
    Feb 21, 2021 at 16:40
  • 1
    @deezg: Arguments can be made either way. Effectively, the difference is on what layer you choose to use the mediator, and that's not set in stone. I tend to have my controller send out commands, so I'm inclined to let the controller fetch the resource then, but that doesn't mean I think the alternatives are inherently bad.
    – Flater
    Feb 22, 2021 at 1:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.