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

aw1*_*975 10 c# entity-framework repository-pattern eager-loading entity-framework-core

我的应用程序使用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);
    }
Run Code Online (Sandbox Code Playgroud)

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

productRepository.GetById(2, p => p.Orders, p => p.Parts);
Run Code Online (Sandbox Code Playgroud)

我想增强此方法以支持嵌套深度超过一个级别的实体的预先加载.例如,假设a Order有自己的LineItem's 集合.

在EF7之前,我相信以下内容也可以检索与每个订单关联的LineItem:

productRepository.GetById(2, p => p.Orders.Select(o => o.LineItems), p => p.Parts);
Run Code Online (Sandbox Code Playgroud)

但是,EF7似乎不支持这一点.相反,有一个新的ThenInclude方法可以检索其他级别的嵌套实体:

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

我不确定如何更新我的存储库以支持使用多个级别的预先加载的实体进行检索ThenInclude.

Ste*_*eve 8

这是一个老问题,但由于它没有一个公认的答案,我以为我会发布我的解决方案.

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

我目前的实现只支持IQueryable方法(即FirstOrDefault,Where基本上标准的lambda函数),但我敢肯定,你可以用它来通过您的具体资料库的方法.

我从EF Core的源代码开始,EntityFrameworkQueryableExtensions.cs这是定义IncludeThenInclude扩展方法的地方.不幸的是,EF使用内部类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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

TEntity泛型类型是接口我的EF实体.在的implmentation 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);
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

现在对于实际允许多个Include和的部分ThenInclude.我有几个扩展方法采取和返回,并IIncludableJoin允许方法链接.在其中我称之为EF IncludeThenIncludeDbSet上的方法:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

就是这样.使用方法如下:

IAccount account = _accountRepository.Join(x=>x.Subscription).Join(x=>x.Addresses).ThenJoin(x=>x.Address).FirstOrDefault(x => x.UserId == userId);
Run Code Online (Sandbox Code Playgroud)

以上将加载基本Account实体,它是一对一的孩子Subscription,它是一对多的子列表Addresses,它是孩子Address.沿途的每个lambda函数都是强类型的,并由intellisense支持,以显示每个实体上可用的属性.


Mor*_*avi 6

您可以将其更改为以下内容:

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);
}
Run Code Online (Sandbox Code Playgroud)


你可以像这样使用它:

productRepository.GetById(2, x => x.Include(p => p.Orders)
                                   .ThenInclude(o => o.LineItems)
                                   .Include(p => p.Parts))
Run Code Online (Sandbox Code Playgroud)

  • 我理解你的担忧.使这种"持久性无知"的一种方法是在数据层中创建一些对象来封装你的获取细节(EF Includes),然后让它们实现一些接口.然后,您可以将该接口传递给存储库方法,并让存储库解析已实现的对象,然后调用该对象中的包含.设置它有点工作,但这就是我在项目中实现预先加载的方式. (2认同)