To cut a long story short, this is mostly a matter of self-imposing a design and then trying to make something fit. Something which is very much needed, but doesn't quite fit with the design we imposed on ourselves.
In the end, it's a matter of finding the compromise in your design that you're comfortable with.
Below, I've listed the common compromises I've encountered in companies/projects I've worked at.
A silly system
In the olden days, repositories were intended to be "entity boxes". You get the
Person from the
PersonRepository, you get the
Car from the
CarRepository, and so on. From a purely coding perspective, this is the cleanest way to organize your datastores.
But then database got significantly better at relational queries, and the "entity box" approach specifically breaks the relational nature of your entities when you're trying to receive multiple levels of entities in a single optimized query. That's a problem
And now we run into one of the silliest problems: self-imposed categorization. It's not that we can't write a query that fetches a list of people and the cars they own, it's that according to our restrictive "entity box" approach, we've actually made it impossible to properly categorize this.
This is no different from labelling all the shelves in your kitchen, only to realize that you now have another food that you didn't put on any label before, and now you refuse to put that food in your kitchen because it doesn't fit with any of your labels. It's downright ridiculous and essentially a jobsworth kind of discussion.
Sticking with repositories
Since this is purely an issue because of a self-imposed pattern, creating the query isn't difficult, and justifying how it fits in your design is a matter of argumentation. You could take the purist route:
- We should stick to the repository pattern as intended. We get the
Person from the
PersonRepository, we get the
Car from the
CarRepository. No relational two-entity-types-in-one-query tricks.
Which is great for code organization, but you're going to run into significant performance issues for large data sets. If the performance becomes unbearable, this option becomes very hard to justify for what is essentially an arbitrary self-imposed rule.
Okay, so let's decide to create this query. Where do we put it? You could find a reason to justify why it belongs in one of the existing repositories:
- Since it returns
Person with nested
Car object, I consider this a
Person-based query and therefore I put it in the
- I consider the owner (i.e. the
Person) as car data, so the query itself is
Car-based and therefore I put it in
These justifications are all, well, justifiable, but you tend to run into issues where one developer thinks about the same query in a different light, and they disagree on which repository the query belongs in.
So to please these disagreeing developers, we could come up with a compromise:
- Since this query uses both
Car entities, we should put this in its own
This sounds great initially, but given many entity types and many queries which fetch several entity types, the amount of repositories is going to explode. Also, if you ever expand one query, it may need to move repository because it now includes another minor entity type. Is that really worth all the effort?
None of these solutions have really worked well, or at least it's very hard to get everyone to agree on a given approach. Maybe we should move away from repositories, at least partially?
At a very basic level, query objects are repositories that only contain one query. Instead of naming them after the entity type (
CarRepository), you name them after the query itself (
GetCarsForOwnerQuery), but the implementation remains virtually the same.
This actually solves the categorization problem. We no longer deal with silly labels in which our custom query doesn't fit, and instead make a new custom label for our custom query.
It also plays very nicely with CQRS, which you mentioned in your question.
Note that you could decide to refactor all your repositories into query (and command) objects, but it's not necessary to do so. You already touched on this idea:
lets say that i follow cqrs for query side ( lets skip command as commands and updates go through repository)
I tend to take such a hybrid approach:
- Single-entity-type queries and commands remain in their entity repository. Personally, I use a generic base repository that provides all the CRUD basics to work with EF tables. It's quick and easy, but not really customization-friendly for very unique entities.
- Multi-entity-type queries and commands get their own query/command object.
When beginning development, you tend to rely heavily on your repositories as you're crudely making entities. But over time, as the application gets fleshed out, you tend to shift more towards query objects.
Whether you use the hybrid approach or move over to using query/command objects entirely is up to you. Using only query/command objects takes a bit more work to type it all out, but you may prefer to stick to one system.
Now I end up having:
- CustomerRepository (DAL Layer)
- CustomerService (BLL - service layer) which is just exposed repository methods and maybe some other related things
- CustomerQueries class (BLL - service layer)
That's not how I would do it. These query objects replace the repositories, so they should live with (or instead of) the repositories, i.e. in the DAL.
A more appropriate approach would be
CustomerRepository (DAL) - for all simple CRUD operations
[myQueryName]Query (DAL) - each of these classes contains a single multi-entity-type query
CustomerService (BLL) can connect to both the repositories or the query objects, depending on what it needs. The service can in principle mix and match to its liking, though usually you tend to create query objects that precisely match a specific
CQRS and query objects
which layer CQRS live? in (business layer which some call it service layer?) or in DAL Layer where my repositories live
Either, both, or neither.
The query objects in the DAL, which is a form of CQRS, are technically unrelated to any CQRS you may have going on in the Business or Domain layers. These are two separate design decisions. You can have:
- a monolith domain service and DAL query objects/commands
- domain queries/commands and monolith repositories
- a monolith domain service and and monolith repositories
- domain queries/commands and DAL query objects/commands.
These are two separate design decisions that do not depend on each other. The thought process behind either is very similar, but you're not forced to make the same decision twice.