Using repository pattern to eager load entities using ThenIclude
Asked Answered
D

3

15

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.

Dacoit answered 26/5, 2016 at 14:6 Comment(0)
A
14

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.

Attalanta answered 12/5, 2017 at 1:50 Comment(5)
Will there be a performance drawback for Joining first and then selecting (via FirstOrDefault() compared to selecting first?Mosira
@Mosira I don't think so? I don't think it matters because when you sit in an IQueryable the query isn't executed until you call .ToList or similar. So the order of operations probably doesn't matter, but I haven't tested it.Attalanta
@Attalanta would you have this somewhere available for a quick peek?Throw
@Throw I'm not sure what you mean? It's used in an internal project that isn't public, but all the code is in the answer.Attalanta
@Attalanta yes I just realised, my bad. I’ll be trying it out on my own project, thanks!Throw
A
9

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))
Anthropomorphosis answered 26/5, 2016 at 15:47 Comment(3)
Thanks for taking the time to respond. The issue I have with your proposed solution is that it exposes IQueryable/Entity Framework calls outside of the repository. I prefer not to do this.Dacoit
I understand your concern. One way to make this "persistence ignorance" is to create some objects in your data layer to encapsulate your fetch details (EF Includes) and then have them implement some interfaces. Then you can pass that interface to your repository method and have the repository to resolve the implemented object and then invoke the includes within that object. It's a bit of work to set that up but that's how I implement eager loading in my projects.Anthropomorphosis
This misses the point. This code requires Include to be a known method beyond the boundaries of the repository.Magenmagena
C
-1

improving the code Morteza Manavi

You can change it to something like this:

 public async Task<TEntity> GetFirstAsync(Expression<Func<TEntity, bool>> where, Func<IQueryable<TEntity>, IQueryable<TEntity>> include, bool disableTracking = true)
    {
        IQueryable<TEntity> result = _entities;

        if (disableTracking)
            result = result.AsNoTracking();

        IQueryable<TEntity> resultWithEagerLoading = include(result);

        return await resultWithEagerLoading.FirstOrDefaultAsync(where);
    }

And you can use it like this:

 var publication = await _unitOfWork.PublicationRepository.GetFirstAsync(x => x.Id == id,
          t => t.Include(p => p.Currency)
              .Include(p => p.PublicationAttributes).ThenInclude(a => a.Attribute)
              .Include(p => p.PublicationLikes).ThenInclude(f => f.From).ThenInclude(i => i.Image));
Choochoo answered 9/2, 2023 at 11:1 Comment(1)
This misses the point. This code, and the code you refer to, requires Include to be a known method beyond the boundaries of the repository.Magenmagena

© 2022 - 2024 — McMap. All rights reserved.