Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.
Entity Framework Core on the Query Side of CQRS… Or Something Else? Well, a video was posted on this topic, and a member of my channel asked me my thoughts. I started watching it, and I realized I was talking out loud saying “pushing clean architecture and indirection”. So, instead, I decided to record my thoughts and provide feedback. So here we go.
Check out my YouTube channel, where I post all kinds of content accompanying my posts, including this video showing everything in this post.
The video I was reviewing was EF Core In The CQRS Query Side… Or Something Else? from Milan Jovanović. I’ll cover the video some of the ideas brought forward, and my thoughts on them. I reached out to Milan before posting my review video, which he was fine with me doing.
From what I gather, this video is intended to illustrate how you can apply Clean Architecture for the query side of CQRS.
Useless ORM Abstraction
The first point of contention I have with almost all illustrations of Clean Architecture with C# and specifically using Entity Framework Core is using an interface to abstract the DbContext.
The problem with this is you’re still depending on EF Core because of the use of a DBSet<T>. This is always exposed because you won’t re-invent all its functionality, nor should you. This interface is trying to abstract EF Core, however, it’s not. This is a useless abstraction simply because you want dependencies to point in a certain direction.
Repositories vs ORM
Another point made in the video is that it may be a downside because it can be confusing that you’ll have two different ways of accessing data. If, on the query side, you’re using your ORM directly (even with the IApplicationDbContext), you’re now introducing two different ways for data access because your command/write side is using Repositories.
This isn’t a downside or confusing. That’s the point of CQRS. Commands and Queries have different concerns. Queries are very specific use cases and the data that needs to be retrieved. Go get the data in the simplest way possible.
The command side about making state changes. That often requires consistency, in which you may use an Aggregate. To retrieve an aggregate, a Repository can be used. Check out my post Should you use the Repository Pattern? With CQRS, Yes and No!
I think persistence ignorance is wildly misunderstood and often hypocritical. I say this because you’ll often see data models also trying to act as rich domain models when using an ORM. Oftentimes you’re modeling your data first in respect to how that can map to your underlying ORM. Then behaviors are tacked on top of that. Which is the exact opposite of what you should be doing if you want to create a rich model. You start with focusing on behavior, capturing the data behind that and then how you persist it. But at the end of the day, how you persist data matters and can affect the design.
The example given to accomplish persistence ignorance on the query side is to take the database call that was in our query handler and move it to its own class.
Now this handler is just a proxy for calling the “IOrderService”. Pet peeve, not everything is a service nor should it be called that. This is just useless indirection. The IOrderService is returning the exact response the Handler needs to return. There’s no value here, just indirection.
This goes on to add other methods to the IOrderService implementation for other query handlers.
Because there’s no cohesion between methods of the IOrderService, it then gets broken down further so that each Query handler now has a specific “Service” for the exact database query that needs to be performed. Here was the existing IOrderService
That then was split into two separate interfaces each having it’s own method.
A class with one method is a function. And we’ve now gone two levels deep of a function, calling a function and doing nothing else. Useless indirection.
“Vertical Sliced” is also mentioned a bit in this video, but I think it’s confusing or what it considers a vertical slice is. A vertical slice isn’t “share nothing” or that you’ll have a single usage based on the example above. The example above using the IGetOrderById has a single usage because it’s a bad example. I have a fairly lengthy talk Vertical Slice Architecture, not Layers!, you should check out to see how Vertical Slices work in comparison to layers.
Coupling & Indirection
A lot of the indirection created was trying to avoid coupling to our data access because of Clean Architecture, or rather direction of dependencies. That’s the point that needs to be addressed. Coupling. If you have a query handler that’s coupled because you’re using a DbContext directly, that’s only a problem if you have a high degree of coupling in other query handles to that same DbContext. If you need to replace data access with some other tooling, then you’re going to have to change it in all usages in all query handlers. However, the same will be true if you’re DbContext is used in the single-use classes, as the example above in IGetOrderById and IGetOrderSummary. You’re still just as coupled, but with more indirection.
Coupling isn’t bad on its own. What you should be paying attention to is the degree of coupling.
Developer-level members of my Patreon or YouTube channel get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.