使用存储库模式使用 ThenIclude 急切加载实体

Using repository pattern to eager load entities using ThenIclude

我的应用程序使用 Entity Framework 7 和存储库模式。

存储库上的 GetById 方法支持预先加载子实体:

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);
}

检索产品(其 id 为 2)以及与该产品关联的订单和零件的用法如下:

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

我想增强此方法以支持预加载嵌套深度超过一层的实体。例如,假设 Order 有自己的 LineItem 集合。

在 EF7 之前,我相信以下方法也可以检索与每个订单关联的 LineItems:

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

但是,EF7 似乎不支持此功能。相反,有一个新的 ThenInclude 方法可以检索额外级别的嵌套实体:

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

我不确定如何更新我的存储库以支持使用 ThenInclude 检索多级预加载实体。

你可以改成这样:

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);
}


你可以这样使用它:

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

这是一个有点老的问题,但由于它没有公认的答案,我想我会 post 我的解决方案。

我正在使用 EF Core 并希望做到这一点,从我的存储库外部访问预先加载 class 这样我就可以指定每次调用存储库方法时要加载的导航属性。因为我有大量的表和数据,所以我不想要一组标准的急切加载实体,因为我的一些查询只需要父实体而一些需要整个树。

我当前的实现仅支持 IQueryable 方法(即 FirstOrDefaultWhere,基本上是标准的 lambda 函数),但我相信您可以使用它传递给您的特定存储库方法。

我从 EF Core 的 EntityFrameworkQueryableExtensions.cs 的源代码开始,其中定义了 IncludeThenInclude 扩展方法。不幸的是,EF 使用内部 class IncludableQueryable 来保存先前属性的树,以允许稍后包含强类型。但是,此实现只不过是 IQueryable 为前一个实体添加了一个额外的通用类型。

我创建了我自己的版本,我称之为 IncludableJoin,它将 IIncludableQueryable 作为构造函数参数并将其存储在私有字段中以供以后访问:

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;
    }
}

注意内部 GetQuery 方法。这在以后很重要。

接下来,在我的通用 IRepository 界面中,我定义了预先加载的起点:

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

TEntity 泛型类型是我的 EF 实体的接口Join 方法在我的通用存储库中的实现是这样的:

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);
    }
    ...
}

现在是实际允许多个 IncludeThenInclude 的部分。我有几个扩展方法,它们采用 return 和 IIncludableJoin 来允许方法链接。我在其中调用 DbSet 上的 EF IncludeThenInclude 方法:

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);
    }
}

在这些方法中,我使用上述 GetQuery 方法获取内部 IIncludableQueryable 属性,调用相关的 IncludeThenInclude 方法,然后return创建一个新的 IncludableJoin 对象以支持方法链接。

就是这样。 this的用法是这样的:

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

以上将加载基础 Account 实体,它是一对一的子实体 Subscription,它是一对多的子列表 Addresses 和它的子实体 Address.沿途的每个 lambda 函数都是强类型的,并受智能感知支持,以显示每个实体的可用属性。