Using repository pattern to eager load entities using ThenIclude

c# eager-loading entity-framework entity-framework-core repository-pattern

Question

My application uses Entity Framework 7 and the repository pattern.

The GetById method on the repository supports eager loading of child entities:

    public virtual TEntity GetById(int id, params Expression<Func<TEntity, object>>[] paths)
    {
        var result = this.Set.Include(paths.First());
        foreach (var path in paths.Skip(1))
        {
            result = result.Include(path);
        }
        return result.FirstOrDefault(e => e.Id == id);
    }

Usage is as follows to retrieve a product (whose id is 2) along with the orders and the parts associated with that product:

productRepository.GetById(2, p => p.Orders, p => p.Parts);

I want to enhance this method to support eager loading of entities nested deeper than one level. For example suppose an Order has its own collection of LineItem's.

Prior to EF7 I believe the following would have been possible to also retrieve the LineItems associated with each order:

productRepository.GetById(2, p => p.Orders.Select(o => o.LineItems), p => p.Parts);

However this doesn't appear to be supported in EF7. Instead there is a new ThenInclude method that retrieves additional levels of nested entities:

https://github.com/aspnet/EntityFramework/wiki/Design-Meeting-Notes:-January-8,-2015

I am unsure as to how to update my repository to support retrieval of multiple-levels of eager loaded entities using ThenInclude.

1
11
5/26/2016 2:06:23 PM

Accepted Answer

This is a bit of an old question, but since it doesn't have an accepted answer I thought I'd post my solution to this.

I'm using EF Core and wanted to do exactly this, access eager loading from outside my repository class so I can specify the navigation properties to load each time I call a repository method. Since I have a large number of tables and data I didn't want a standard set of eagerly loading entities since some of my queries only needed the parent entity and some needed the whole tree.

My current implementation only supports IQueryable method (ie. FirstOrDefault, Where, basically the standard lambda functions) but I'm sure you could use it to pass through to your specific repository methods.

I started with the source code for EF Core's EntityFrameworkQueryableExtensions.cs which is where the Include and ThenInclude extension methods are defined. Unfortunately, EF uses an internal class IncludableQueryable to hold the tree of previous properties to allow for strongly type later includes. However, the implementation for this is nothing more than IQueryable with an extra generic type for the previous entity.

I created my own version I called IncludableJoin that takes an IIncludableQueryable as a constructor parameter and stores it in a private field for later access:

public interface IIncludableJoin<out TEntity, out TProperty> : IQueryable<TEntity>
{
}

public class IncludableJoin<TEntity, TPreviousProperty> : IIncludableJoin<TEntity, TPreviousProperty>
{
    private readonly IIncludableQueryable<TEntity, TPreviousProperty> _query;

    public IncludableJoin(IIncludableQueryable<TEntity, TPreviousProperty> query)
    {
        _query = query;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public IEnumerator<TEntity> GetEnumerator()
    {
        return _query.GetEnumerator();
    }

    public Expression Expression => _query.Expression;
    public Type ElementType => _query.ElementType;
    public IQueryProvider Provider => _query.Provider;

    internal IIncludableQueryable<TEntity, TPreviousProperty> GetQuery()
    {
        return _query;
    }
}

Note the internal GetQuery method. This will be important later.

Next, in my generic IRepository interface, I defined the starting point for eager loading:

public interface IRepository<TEntity> where TEntity : class
{
    IIncludableJoin<TEntity, TProperty> Join<TProperty>(Expression<Func<TEntity, TProperty>> navigationProperty);
    ...
}

The TEntity generic type is the interface of my EF entity. The implmentation of the Join method in my generic repository is like so:

public abstract class SecureRepository<TInterface, TEntity> : IRepository<TInterface>
    where TEntity : class, new()
    where TInterface : class
{
    protected DbSet<TEntity> DbSet;
    protected SecureRepository(DataContext dataContext)
    {
        DbSet = dataContext.Set<TEntity>();
    }

    public virtual IIncludableJoin<TInterface, TProperty> Join<TProperty>(Expression<Func<TInterface, TProperty>> navigationProperty)
    {
        return ((IQueryable<TInterface>)DbSet).Join(navigationProperty);
    }
    ...
}

Now for the part that actually allows for multiple Include and ThenInclude. I have several extension methods that take and return and IIncludableJoin to allow for method chaining. Inside which I call the EF Include and ThenInclude methods on the DbSet:

public static class RepositoryExtensions
{
    public static IIncludableJoin<TEntity, TProperty> Join<TEntity, TProperty>(
        this IQueryable<TEntity> query,
        Expression<Func<TEntity, TProperty>> propToExpand)
        where TEntity : class
    {
        return new IncludableJoin<TEntity, TProperty>(query.Include(propToExpand));
    }

    public static IIncludableJoin<TEntity, TProperty> ThenJoin<TEntity, TPreviousProperty, TProperty>(
         this IIncludableJoin<TEntity, TPreviousProperty> query,
         Expression<Func<TPreviousProperty, TProperty>> propToExpand)
        where TEntity : class
    {
        IIncludableQueryable<TEntity, TPreviousProperty> queryable = ((IncludableJoin<TEntity, TPreviousProperty>)query).GetQuery();
        return new IncludableJoin<TEntity, TProperty>(queryable.ThenInclude(propToExpand));
    }

    public static IIncludableJoin<TEntity, TProperty> ThenJoin<TEntity, TPreviousProperty, TProperty>(
        this IIncludableJoin<TEntity, IEnumerable<TPreviousProperty>> query,
        Expression<Func<TPreviousProperty, TProperty>> propToExpand)
        where TEntity : class
    {
        var queryable = ((IncludableJoin<TEntity, IEnumerable<TPreviousProperty>>)query).GetQuery();
        var include = queryable.ThenInclude(propToExpand);
        return new IncludableJoin<TEntity, TProperty>(include);
    }
}

In these methods I am getting the internal IIncludableQueryable property using the aforementioned GetQuery method, calling the relevant Include or ThenInclude method, then returning a new IncludableJoin object to support the method chaining.

And that's it. The usage of this is like so:

IAccount account = _accountRepository.Join(x=>x.Subscription).Join(x=>x.Addresses).ThenJoin(x=>x.Address).FirstOrDefault(x => x.UserId == userId);

The above would load the base Account entity, it's one-to-one child Subscription, it's one-to-many child list Addresses and it's child Address. Each lambda function along the way is strongly typed and is supported by intellisense to show the properties available on each entity.

8
5/12/2017 1:50:41 AM

Popular Answer

You can change it to something like this:

public virtual TEntity GetById<TEntity>(int id, Func<IQueryable<TEntity>, IQueryable<TEntity>> func) 
{
    DbSet<TEntity> result = this.Set<TEntity>();

    IQueryable<TEntity> resultWithEagerLoading = func(result);

    return resultWithEagerLoading.FirstOrDefault(e => e.Id == id);
}


And you can use it like this:

productRepository.GetById(2, x => x.Include(p => p.Orders)
                                   .ThenInclude(o => o.LineItems)
                                   .Include(p => p.Parts))


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow