3

From many articles and answers on DDD Repository pattern, I got the feeling that a Repository should only CURD an Entity (Aggregate Root) as a whole.

Following this convention, we always need to query the database to get all required data to construct an Entity. Following is an example implementation when you want two properties of the Entity:

class ProductInfoDto
{
    public string ProductName {get;set;}
    public decimal ProductPrice {get;set;}
}

class ProductRepository : IProductRepository
{
    Product GetProduct(int productId)
    {
        //query database to fetch all data needed and create a Product
    }
}

class ProductReportService : IProductReportService
{
    IProductRepository _repo;

    public ProductInfoDto GetProductInfo(int productId)
    {
        var product = _repo.GetProduct(productId);
        return new ProductInfoDto(product.Name, product.Price);
    }
}

Actually we can just query the database to get Name and Price and create ProductInfoDto to improve query performance (since there are less data need to be fetched):

class ProductInfoDto
{
    public string ProductName {get;set;}
    public decimal ProductPrice {get;set;}
}
class ProductRepository : IProductRepository
{
    ProductInfoDto GetProductInfo(int productId)
    {
        //query database to fetch product name and price
        return new ProductInfoDto(productNameFromDb, productPriceFromDb);
    }
}

class ProductReportService : IProductReportService
{
    IProductRepository _repo;

    public ProductInfoDto GetProductInfo(int productId)
    {
        var productInfo= _repo.GetProductInfo(productId);
        return productInfo;
    }
}

But this breaks the convention that Repository should only CURD an Entity (Aggregate Root) as a whole. This implementation does have drawbacks that will make the Repository fragmented and bloated with many methods and DTOs, as the business logic on the Entity becomes more and more complicated.

My major concern is the performance issue. What should I do to implement the Repository properly? Why?


UPDATE about bypassing the repository said by VoiceOfUnreason:

the option of bypassing the repository and fetching the information you need more directly.,

I think CQRS Query also uses Repository to fetch required data. And to follow the principle of *Persistence Ignorance, EFCore DbContext or ADO.NET should only be used in Repository to keep Query from affected by Persistence.

3
  • 2
    This question is an easy one to answer if you just drop the assumption that your repository must return the entire entity or aggregate root. You are then free to custom tailor your repository as you see fit. Commented Apr 15, 2023 at 17:17
  • 1
    That's not an assumption. It's recommended by many experts and I want to know why and how they solve this issue in practice.
    – zwcloud
    Commented Apr 16, 2023 at 2:51
  • @zwcloud Because DDD looks great on paper but not really in implementation? I suggest you to treating it like a "toolbox" and grab what you need, instead of following mentally. Commented Nov 8, 2023 at 4:06

3 Answers 3

1

You might find it useful to look into [CQRS].

But this breaks the convention that Repository should only CURD an Entity (Aggregate Root) as a whole.

Probably. But why are you insisting that the repository be part of the flow?

You've already introduced the idea of a facade that can be used to obtain a ProductInfoDto from a productId. Implementations hide from their clients the details of how, precisely, this is achieved -- and one of the advantages of that is that you can change the details of how it is done without that change cascading throughout your application.

In cases where performance isn't critical, you can of course do "the simplest thing that could possibly work", and have the implementation delegate the details of lookup to the underlying repository.

But if performance is critical, you have the option of bypassing the repository and fetching the information you need more directly.

It's not free, of course - at a minimum, you are giving up the conceptual simplicity of "all data access happens via the repository and the aggregate root", you might also end up with some duplication because the repository and the whatever-does-the-job-directly need, somewhere, a shared understanding of what is going on.


Or you can instead decide (as noted above) that this is a capability that should be provided by the repository, even if it doesn't fit the mold of CURD and summary statistics that would be sufficient elsewhere.

(Expressed another way: REPOSITORY is a pattern; you are supposed to be able to modify it to fit different contexts.)


One important idea to keep in mind; obtaining an unlocked copy of some information known to the model is fundamentally different from modifying information known to the model - especially if that model is supposed to maintain some invariant on the data.

Reads and writes are different.

2
  • As you said, I think the best choice is to modify the Repository to adapt to CQRS Query/Command or business service(when CQRS is not applied). Adding Repository methods to return/pass DTOs is acceptable.
    – zwcloud
    Commented Apr 16, 2023 at 6:37
  • Updated the question to add my thoughts about bypassing the repository and why I don't think it's a good choice. My understanding might be incorrect. Please shine some light on that.
    – zwcloud
    Commented Apr 16, 2023 at 6:52
1
  1. I got the feeling that a Repository should only CURD an Entity (Aggregate Root) as a whole.
  2. Following this convention, we always need to query the database to get all required data to construct an Entity.

2 is the correct logical consequence of 1, but this should've also been the red flag that 1 is maybe not as correct as you seem to think it is.

If I want to get a list of the 5 most common surnames in the country (let's assume my database contains all citizens), does that mean I would have to load every single citizen's complete details before I could find the 5 most common surnames?
Clearly, that is a waste of bandwidth and couldn't possibly be considered the right way to approach this.

Repositories are not inherently limited to CRUD-only. Repositories are commonly bounded by a particular storage element, often a database table (but multiple tables is possible too), but there's no particular limit on which kinds of operations the repository can contain.

Some of these methods might return a partial entity. Some of them might return a primitive (e.g. a boolean that indicated if an entry exists or not). These are just some examples, this is by no means a closed list.

This implementation does have drawbacks that will make the Repository fragmented and bloated with many methods and DTOs, as the business logic on the Entity becomes more and more complicated.

Somewhat contrary to my previous point, this is also true. So, in the interest of completeness, let me sum up:

  • It is perfectly okay for a repository to contain non-CRUD methods.
  • When there are many non-CRUD methods, you might want to consider a different pattern, for the reasons you pointed out above.

My major concern is the performance issue. What should I do to implement the Repository properly? Why?

The repository pattern or any of its alternatives make no impact on performance.
In either case, it's perfectly fine for you to develop methods that only fetch the data you actually need from the database, which means that the performance-specific concerns are resolved in either case.

I think CQRS Query also uses Repository to fetch required data.

CQRS is not inherently tied to the DAL. You could implement CQRS on the BLL (which has no bearing on how you design your DAL), or you could implement CQRS on the DAL (commonly referred to as query objects).

Assuming you're talking about CQRS on the DAL (i.e. query object), these can defer to repositories that are in the DAL, or they can access the database directly. Both make sense.

Personally, I tend to always have some generic CRUD repository available just for ease of access when I'm trying to perform trivial and highly reusable database operations (most commonly, this happens for commands rather than queries). But for anything non-trivial, I tend to put that in the query object directly (most commonly, this happens for queries, not really commands).

If the concrete class is in the DAL, it can access the data store directly. Whether said concrete class is a repository or a CQRS handler is an irrelevant distinction. It's in the DAL, therefore it gets to access the data store if it wants to access it.

And to follow the principle of *Persistence Ignorance, EFCore DbContext or ADO.NET should only be used in Repository to keep Query from affected by Persistence.

This is a contentious issue. You're not wrong, but not everyone agrees on this topic.

Personally, while I very much appreciate the kind of abstraction you get from repositories and units of work, Entity Framework's DbSet<T> is a repository, and the DbContext is a unit of work. It doesn't always make sense to wrap these in another layer of a custom-built repository/unit of work layer.

Sometimes it does make sense, e.g. when the choice of ORM (or lack thereof) is considered highly volatile. But in reality, companies hardly if ever shift their storage technology, and with EF's increasing range of supported providers, it's getting harder to justify the additional complexity of a secondary layer when the odds of actually migrating to an incompatible data store are slim to none.

1

Repository is concerned with abstracting the persistence environment that could be of any type: database, data grid, file system or whatever stores data in a non-volatile manner, and it should support any form of querying that provides information requested by the caller. It is common for repositories to use implementation of a persistence specification that provide methods for lazy fetching, getting partially specific NPersistence for C# or JPA for Java which include EntityManager.GetReference and EntityManager.getReference methods.

The pattern that recommends querying an entire entity is active record pattern.

1
  • Your answer reminds me of the Specification pattern. Although it also breaks the convention, it is a good alternative to solve the problem. Thank you!
    – zwcloud
    Commented Apr 16, 2023 at 8:54

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.