Reuse handlers with MediatR?
Hi,
I would like to know the best approach to share the same code in a codebase with CQRS and MediatR. Suggest a simple CRUD. You have a GetById, but also an Update and Delete. For the Update and Delete handlers I also have to retrieve the entity first, but they use the same logic as the GetById handler. Now I was wondering what the best approach is to do this with principles in mind, such as DRY, SRP and the fact that you don't want to have too many dependencies to keep it modular. In short, is it common to call the Update and Delete handler into the GetById handler, or re-implement the GetById logic in the Update and Delete handler, or should I create an additional service where all those handlers get this logic from. The last option seems best to me, I think.
I'd love to hear from you!
20 Replies
You could extract the
GetById
logic somewhere else. Like a service, sure
Or an extension method on the IQueryableWhy do you have to retrieve the entity for update/delete? Are you doing a simple existence check?
@Rory
That's not worth abstracting away in any way, shape, or form lmao
I thought you have, like, 30
.Where()
s thereHaha nah, just a really simple case. But I would like to know the 'best practices'
I really like to keep things WET
I only ever consider abstracting some shared code away if it's more than 6-7 lines or so
I heard someone telling me that u should abstract something if you have it on a minimum of 3 different places
Ok I'm no EF guy so can't help with specifics but speaking in the abstract "Get a full resource by ID, which might involve joins etc" and "delete or update this thing but let me know if it doesn't exist" are different operations. The delete/update don't rely on the read model at all, just an ID check, so no need to have cross-dependency with the query
Yeah, delete and update can be done with
.Execute
methodsWhich would lead me to suggest you don't have it in three places.
But also agree with @Angius 's ethos - DRY isn't about "don't write the same line of code twice", it's about having a single source of truth for meaningful things.
The best solution is to not use mediator and instead make a separate pipeline that you compose to handle this, but no one does this for some reason. The reason you need this is because it's the most general way to combine sequential logic and you can share the steps. The pipeline behaviors offered by mediator still require a handler class and can't be applied in a specific order / for specific groups of handlers. I mean, you kinda can, but don't, it's a mess. I have tried. And they don't have a shared context type. So you have to put all your context in the request. But it's not for that at all.
The other way is to make a generic base class that handles it for an entity. Then subclass for each handler. This is bad because slight variations in the logic cannot be added by just modifying the pipeline, it has to be solved by introducing virtual steps in the main method in the base class.
You'll still have to copy paste the controller code. Nothing you can do about that really if you're just using attributes on the actions.
My take is — mediator is trash, just have one action per controller. But even then you'll have to copy paste the controller code.
In short, the "best practices" is to not care and copy paste from what I've seen.
Wasn't .Execute mostlikely for bulk updates/deletes? I also think it had some side effects. Maybe I'm wrong tho
It works just as well for single items, it doesn't generate some super special query
I don't know of any side effects either
I citate GPT 😄 "The side effects of using ExecuteUpdate or ExecuteDelete primarily revolve around bypassing EF Core's tracking and lifecycle mechanisms. While they offer significant performance benefits for bulk operations, they should be used cautiously in scenarios requiring tracking, validation, or business logic tied to lifecycle events."
Would recommend documentation and not gpt lol
I don't know if that's accurate but if we need to nail down objectivity, the LLM aint it
So when using .Execute, it will automatically save in the database instead of waiting on saveChanges
Yea fair enough, but the simplicity sometimes :/
You can still manually wrap it in a transaction if you wanted
So it sounds like if you're not taking advantage of EF change tracking you're good? And it sounds more like entity framework's change tracking slows down bulk operations, which this bypasses? Rather than "those endpoints are bulk only"
I'm no expert also but this feels more like "CRUD via MediatR" rather than CQRS, might be worth reading more into those