Question

I'm wondering how to properly handle eager-loading problem for complex object graphs when using Repository pattern. This isn't ORM specific problem i guess.

First try:

public interface IProductRepository : IRepository<Product>
{
  Product GetById(int id);
  IProductRepository WithCustomers();
}

This would work fine, but that would involve repeating myself all the time (writing custom 'With' methods in repository implementations everywhere).

Next approach:

public interface IRepository<T> where T : IAggregateRoot
{
  ...
  void With(Expression<Func<T, object>> propToExpand);
}

With method will add an item to private collection which will be used later to find out what props should be eager loaded when retrieving necessary entity/ies.

This kind a works and is fine. But i dislike usage:

productRepository.With(x=>x.Customer);
productRepository.With(x=>x.Price);
productRepository.With(x=>x.Manufacturer);
var product = productRepository.GetById(id);

Basically - problem is that there isn't chaining. I would like it to be like this:

var product = productRepository
  .With(x=>x.Customer)
  .With(x=>x.Price)
  .With(x=>x.Manufacturer)
  .GetById(id);

I couldn't achieve this. Even if i could - i'm not sure if that solution would be elegant.

This leads to thoughts that i'm missing something fundamental (lack of examples anywhere). Are there different ways how to handle this? What are best practices?

Was it helpful?

Solution

Interesting problem and I am sure you are not the first one having trouble with this (I absolutelty have).

For me, the real question is: where do you want to put your eager loading logic?

Outside of the repository in the client code

var product = productRepository
.With(x=>x.Customer)
.With(x=>x.Price)
.With(x=>x.Manufacturer)
.GetById(id);

I dont think that is good software design: it looks like this could cause "death by a thousand cuts" if such constructs are scattered through your whole app.

Or within the repository. Example:

interface IProductRepository {
    Product GetById(int id);
    Product GetByIdWithCustomers(int i);
}

So your client code would look like this:

var product = productRepository.GetByIdWithCustomers(id);

Normally I make one BaseRepository which has just the basic CRUD operations defined:

public class BaseRepository<TEntity, TPrimaryKey> {
    public void Save(TEntity entity) { ... }
    public void Delete(TEntity entity) { ... }
    public TEntity Load(TPrimaryKey id) { ... } // just gets the entity by primary key
}

Then I extend this base Class / Interface in order to provide specific methods for fetching domain objects. Your approach seems to go in a somewhat similiar direction.

public class MediaRepository : BaseRepository<Media, int> {
    public long CountMediaWithCategories() { ... }
    public IList<Media> MediaInCategories(IList<Category> categories) { .... }
}

The good thing: all ORM stuff (eager loading config, fetch depth etc) is encapsulated in the Repository class, the client code just gets the result set.

I tried working with very generic repositories like you are trying to do, but I mostly ended up writing specific queries and repositories for my domain objects.

OTHER TIPS

var product = productRepository
 .With(x=>x.Customer)
 .With(x=>x.Price)
 .With(x=>x.Manufacturer)
 .GetById(id);

I can understand your wish to determine the query depth of the object graph like above but i think there might be an easier way to do it. How about instead of choosing to return a Product (with Customer, Price and Manufacturer) by ID i simply return the Product - and all of those other things are lazy loaded properties of Product?

I achieve this 'complete graph accessibility' by 'chaining' by POCO object model in my data access layer. This way i don't need to know how much eager loaded data to pull out at any one time, i just ask for what i need from the object graph, and the model knows what is loaded and what needs recovering additionally from the DAL. Take a look at these three answers - i try to explain my approach there. If you need more clarification let me know and i'll edit this answer.

It's a old question but perhaps it can help someone. I've spent sometime to find a good aproach, here is what I've found in C#:

IRepository.cs:

public interface IRepository<TEntity> where TEntity : class
{
    IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> where
                              , params Expression<Func<TEntity, object>>[] properties);
}

Repository.cs

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{    
    private readonly DbSet<TEntity> _dbset;

    public Repository(DbSet<TEntity> dbset)
    {
        _dbset = dbset;
    }

    public virtual IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> where
                              , Expression<Func<TEntity, object>>[] properties)
    {
        if (where == null) 
            throw new ArgumentNullException(nameof(where));    
        if (properties == null) 
            throw new ArgumentNullException(nameof(properties));

        var query = _dbset as IQueryable<TEntity>; // _dbSet = dbContext.Set<TEntity>()

        query = properties
                   .Aggregate(query, (current, property) => current.Include(property));

        return query.AsNoTracking().Where(where).ToList();
    }
}

How to use:

var repository = new Repository<User>();
var users = repository.GetAll(p => p.Id == 1, d => d.Address, d => d.Carts);

Ref: Link

I can appreciate what you are trying to do, but you are somewhat beyond the basic repository-pattern.

A minimal repository interface may include methods for:

  • GetById
  • Add
  • Remove

If you add additional methods on top of that, you start running into situations where the Interface doesn't necessarily make sense for all of your aggregate roots.

Sometimes it's just not feasible to have a completely beautiful API. If what you have works "good enough" for you, I would go with it. If you need to get away from the repository pattern to provide a better API to program against, do it!

The repository pattern isn't a be-all / end-all solution. Sometimes you need a different solution.

If you want to indicate all of the Includes you need outside of your repository, you can list optional params (C#) for each generic method:

TEntity Find(Func<TEntity, bool> expression, params string[] eagerLoads);

Then on your client tier:

IProductRepository.Find(x => x.Id == id, "Customer", "Price")

If you want to be type safe, enumerate your entities:

public enum BusinessEntities { Customer, Price, Manufacturer }

IProductRepository.Find(x => x.Id == id, BusinessEntities.Customer.ToString(), BusinessEntities.Price.ToString())

I think that it is the client's responsibility to ask specifically for what it wants. Generic Repository should just handle basic CRUD.

In the BaseRepository.cs you can create this method:

public async Task<IEnumerable<T>> GetWithChild(string child)
{
    return await _entities.Include(child).ToListAsync();
}

In my API I have also implemented a service layer but from the API I simply call this method and pass it the name of the variable to load.

Obviously, in your situation, you'll need to include a few more strings.

I posted an answer earlier but I still wasn't happy with the solution. So here is a better solution.

in the BaseRepository.cs

public async Task<IEnumerable<T>> GetAll(params Expression<Func<T, object>>[] properties)
{
      IQueryable<T> query = _entities;

      query = properties.Aggregate(query, (current, property) => current.Include(property));

      return await query.AsNoTracking().ToListAsync();
}

and you can simply use the method as follows

await _service.GetAll(x => x.Customer, x => x.Price, x => x.Manufacturer); 
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top