Entity Framework Core 2.0.1 急切加载所有嵌套的相关实体

Posted

技术标签:

【中文标题】Entity Framework Core 2.0.1 急切加载所有嵌套的相关实体【英文标题】:Entity Framework Core 2.0.1 Eager Loading on all nested related entities 【发布时间】:2018-09-10 14:52:46 【问题描述】:

我有一个简单的问题,但似乎找不到解决方法。我正在使用 Entity Framework Core 2.0.1 版,并希望默认预先加载我的所有实体。

例子:

public class Order

    public int Id  get; set; 
    public string Name  get; set; 
    public int CustomerId  get; set; 
    public Customer Customer  get; set; 


public class Customer

    public int Id  get; set;  
    public string Name  get; set; 
    public int AddressId  get; set; 
    public Address Address  get; set; 


public class Address

    public int Id  get; set; 
    public string PostCode  get; set; 
    public string City  get; set; 

但是当我加载 Order 实体时,相关实体 Customer 然后在其中 Address 为空

我尝试过的:

尝试升级到 2.1 版并将 LazyLoadingProxies 设置为 false

这只是一个例子,我有多个嵌套级别的实体,我想在通用存储库中加载嵌套的相关数据,所以不能使用 IncludeThenInclude 因为加载时我不知道实际的实体类型。

例子:

    public virtual async Task<IEnumerable<T>> GetAllAsync(Expression<Func<T, bool>> predicate = null)
    
        if (predicate == null)
        
            return await Context.Set<T>().ToListAsync();
        
        return await Context.Set<T>().Where(predicate).ToListAsync();
    

我错过了什么?我在存储库中做错了什么吗?任何有关更好设计的帮助或指示(如果这就是问题所在),我们将不胜感激。

谢谢

【问题讨论】:

所以您将延迟加载设置为 false,不包含导航属性,并且想知道为什么不包含导航属性? (如果默认包含导航属性,Include 的意义何在) 已尝试将我的相关属性标记为虚拟,并通过在我的实体构造函数中注入延迟加载器操作来使用代理启用延迟加载,也可以不使用代理,但效果相同。除非我手动使用 Include 和 ThenInclude(这需要我知道实体类型),否则我似乎无法获取相关数据 您要求的功能目前不存在。我记得,有人要求这样的事情,但我不知道是否以及何时会得到解决。 感谢您的意见@IvanStoev。我认为他们在最新推出的 2.1 版中添加了许多功能。 @Jinish 不幸的是不是这个 :( 这是我发现的 - Eager load all navigation properties #4851(已关闭),Rule-based eager load (include) #2953(积压),Allow for declaring aggregates in the model (e.g. defining included properties or by some other means) #1985(积压)。积压意味着考虑,但没有具体时间表。 【参考方案1】:

目前官方不存在此类功能(EF Core 2.0.2 以及即将到来的 2.1)。已在Eager load all navigation properties #4851(已关闭)中提出请求,目前由Rule-based eager load (include) #2953 和Allow for declaring aggregates in the model (e.g. defining included properties or by some other means) #1985 跟踪(均在积压中,即没有具体时间表)。

我可以提供以下两种自定义扩展方法:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Microsoft.EntityFrameworkCore

    public static partial class CustomExtensions
    
        public static IQueryable<T> Include<T>(this IQueryable<T> source, IEnumerable<string> navigationPropertyPaths)
            where T : class
        
            return navigationPropertyPaths.Aggregate(source, (query, path) => query.Include(path));
        

        public static IEnumerable<string> GetIncludePaths(this DbContext context, Type clrEntityType, int maxDepth = int.MaxValue)
        
            if (maxDepth < 0) throw new ArgumentOutOfRangeException(nameof(maxDepth));
            var entityType = context.Model.FindEntityType(clrEntityType);
            var includedNavigations = new HashSet<INavigation>();
            var stack = new Stack<IEnumerator<INavigation>>();
            while (true)
            
                var entityNavigations = new List<INavigation>();
                if (stack.Count <= maxDepth)
                
                    foreach (var navigation in entityType.GetNavigations())
                    
                        if (includedNavigations.Add(navigation))
                            entityNavigations.Add(navigation);
                    
                
                if (entityNavigations.Count == 0)
                
                    if (stack.Count > 0)
                        yield return string.Join(".", stack.Reverse().Select(e => e.Current.Name));
                
                else
                
                    foreach (var navigation in entityNavigations)
                    
                        var inverseNavigation = navigation.FindInverse();
                        if (inverseNavigation != null)
                            includedNavigations.Add(inverseNavigation);
                    
                    stack.Push(entityNavigations.GetEnumerator());
                
                while (stack.Count > 0 && !stack.Peek().MoveNext())
                    stack.Pop();
                if (stack.Count == 0) break;
                entityType = stack.Peek().Current.GetTargetType();
            
        

    

第一种只是应用多个字符串基Include的便捷方式。

第二个是使用 EF Core 提供的元数据收集类型的所有 Include 路径的实际工作。它基本上是从传递的实体类型开始的有向循环图处理,不包括包含路径的反向导航,并且只发出到“叶”节点的路径。

你的例子中的用法可能是这样的:

public virtual async Task<IEnumerable<T>> GetAllAsync(Expression<Func<T, bool>> predicate = null)

    var query = Context.Set<T>()
        .Include(Context.GetIncludePaths(typeof(T));
    if (predicate != null)
        query = query.Where(predicate);
    return await query.ToListAsync();

【讨论】:

先生,您应该获得奖牌? 你当然值得获得一枚奖牌。这是一个很好的样本!我使用它来帮助我理解迭代以及 EF 核心。我希望您能帮助我理解yield return 行在您的GetIncludePaths 方法中是如何工作的。当他们这样说时,我不太明白 MSDN 的意思:“下次调用迭代器函数时,将从该位置重新启动执行”。 @Lopside 此处的空间有限,但我会尝试。这称为迭代器方法。直到一些外部代码开始foreach-ing 之后,里面的代码才会执行​​。一旦外部代码输入foreach,您的代码就会开始执行。当它遇到某个yield return 语句时,它会将下一个元素返回给调用者。当调用者foreach 需要下一个项目时,您的代码会在yield return 之后的点继续执行并继续执行,直到遇到另一个yield return 或结束。当您的代码结束或调用者退出foreachbreakreturn 或异常)时,整个过程结束。 天哪。什么? 这让我很伤心。【参考方案2】:

伊万的回答太棒了。我已经稍微调整了它(使用 Chistoph 的代码 here 的帮助),这样扩展方法就可以与 DbContext 本身联系起来,以防其他人觉得更方便。例如,在我的代码库中,我可以写:

_commissionsContext.CommissionRulesetScopes.IncludeAll().ToListAsync();

这将急切地为每个CommissionRulesetScope 加载整个实体子图:

SELECT [c].[CommissionPlanId], [c].[StartPeriod], [c].[CommissionRulesetId], [c0].[Id], [c0].[Name], [c1].[Id], [c1].[CsiScoreRuleId], [c1].[DealerOptionCommissionRuleId], [c1].[EmailCaptureRuleId], [c1].[ProductCommissionRuleId], [c1].[ProductConsistencyRuleId], [c1].[UnitCommissionRulesetId], [c2].[Id], [c2].[ConsecutiveFailurePenalty], [c2].[CurrentMonthPenalty], [c2].[Enabled], [c2].[Target], [d].[Id], [e].[Id], [e].[Enabled], [e].[Penalty], [e].[Target], [p].[Id], [p0].[Id], [p0].[CommissionBonus], [p0].[Enabled], [p0].[ProductTarget], [p0].[UnitTarget], [u].[Id], [u].[AverageCsiScoreRuleId], [u].[FinancePenetrationRuleId], [u].[GuaranteePeriodCommissionLevel], [u].[MinimumRequiredCsiReturnRate], [u].[MonthlyExpectationAttainmentRuleId], [u].[UnitCommissionTable], [a].[Id], [f].[Id], [m].[Id], [d0].[DealerOptionCommissionRuleId], [d0].[MinimumValue], [d0].[Commission], [t].[ProductCommissionRuleId], [t].[ProductTypeId], [t].[Commission], [t].[Id], [t].[Description], [t].[Key], [t0].[ProductConsistencyRuleId], [t0].[ProductMinMixRangeId], [t0].[Id], [t0].[ProductTypeId], [t0].[Id0], [t0].[Description], [t0].[Key], [t0].[ProductMinMixRangeId0], [t0].[MinimumUnitsTarget], [t0].[Target], [a0].[RuleId], [a0].[Target], [a0].[Points], [f0].[RuleId], [f0].[Target], [f0].[Points], [m0].[RuleId], [m0].[Target], [m0].[Points]
FROM [CommissionRulesetScope] AS [c]
INNER JOIN [CommissionPlan] AS [c0] ON [c].[CommissionPlanId] = [c0].[Id]
INNER JOIN [CommissionRuleset] AS [c1] ON [c].[CommissionRulesetId] = [c1].[Id]
LEFT JOIN [CsiScoreRule] AS [c2] ON [c1].[CsiScoreRuleId] = [c2].[Id]
LEFT JOIN [DealerOptionCommissionRule] AS [d] ON [c1].[DealerOptionCommissionRuleId] = [d].[Id]
LEFT JOIN [EmailCaptureRule] AS [e] ON [c1].[EmailCaptureRuleId] = [e].[Id]
LEFT JOIN [ProductCommissionRule] AS [p] ON [c1].[ProductCommissionRuleId] = [p].[Id]
LEFT JOIN [ProductConsistencyRule] AS [p0] ON [c1].[ProductConsistencyRuleId] = [p0].[Id]
LEFT JOIN [UnitCommissionRuleset] AS [u] ON [c1].[UnitCommissionRulesetId] = [u].[Id]
LEFT JOIN [AverageCsiScoreRule] AS [a] ON [u].[AverageCsiScoreRuleId] = [a].[Id]
LEFT JOIN [FinancePenetrationRule] AS [f] ON [u].[FinancePenetrationRuleId] = [f].[Id]
LEFT JOIN [MonthlyExpectationAttainmentRule] AS [m] ON [u].[MonthlyExpectationAttainmentRuleId] = [m].[Id]
LEFT JOIN [DealerOptionCommission] AS [d0] ON [d].[Id] = [d0].[DealerOptionCommissionRuleId]
LEFT JOIN (
    SELECT [p1].[ProductCommissionRuleId], [p1].[ProductTypeId], [p1].[Commission], [p2].[Id], [p2].[Description], [p2].[Key]
    FROM [ProductCommission] AS [p1]
    LEFT JOIN [ProductType] AS [p2] ON [p1].[ProductTypeId] = [p2].[Id]
) AS [t] ON [p].[Id] = [t].[ProductCommissionRuleId]
LEFT JOIN (
    SELECT [p3].[ProductConsistencyRuleId], [p3].[ProductMinMixRangeId], [p4].[Id], [p4].[ProductTypeId], [p5].[Id] AS [Id0], [p5].[Description], [p5].[Key], [p6].[ProductMinMixRangeId] AS [ProductMinMixRangeId0], [p6].[MinimumUnitsTarget], [p6].[Target]
    FROM [ProductMinMixRangeAssociation] AS [p3]
    INNER JOIN [ProductMinMixRange] AS [p4] ON [p3].[ProductMinMixRangeId] = [p4].[Id]
    INNER JOIN [ProductType] AS [p5] ON [p4].[ProductTypeId] = [p5].[Id]
    LEFT JOIN [ProductMinMixTarget] AS [p6] ON [p4].[Id] = [p6].[ProductMinMixRangeId]
) AS [t0] ON [p0].[Id] = [t0].[ProductConsistencyRuleId]
LEFT JOIN [AverageCsiScoreThreshold] AS [a0] ON [a].[Id] = [a0].[RuleId]
LEFT JOIN [FinancePenetrationThreshold] AS [f0] ON [f].[Id] = [f0].[RuleId]
LEFT JOIN [MonthlyExpectationAttainmentThreshold] AS [m0] ON [m].[Id] = [m0].[RuleId]
ORDER BY [c].[CommissionPlanId], [c].[StartPeriod], [c0].[Id], [c1].[Id], [d0].[DealerOptionCommissionRuleId], [d0].[MinimumValue], [t].[ProductCommissionRuleId], [t].[ProductTypeId], [t0].[ProductConsistencyRuleId], [t0].[ProductMinMixRangeId], [t0].[Id], [t0].[Id0], [t0].[ProductMinMixRangeId0], [t0].[MinimumUnitsTarget], [a0].[RuleId], [a0].[Target], [f0].[RuleId], [f0].[Target], [m0].[RuleId], [m0].[Target]

这是改编:

public static class DbSetExtensions

    /// <summary>
    /// Ensures that all navigation properties (up to a certain depth) are eagerly loaded when entities are resolved from this
    /// DbSet.
    /// </summary>
    /// <returns>The queryable representation of this DbSet</returns>
    public static IQueryable<TEntity> IncludeAll<TEntity>(
        this DbSet<TEntity> dbSet,
        int maxDepth = int.MaxValue) where TEntity : class
    
        IQueryable<TEntity> result = dbSet;
        var context = dbSet.GetService<ICurrentDbContext>().Context;
        var includePaths = GetIncludePaths<TEntity>(context, maxDepth);

        foreach (var includePath in includePaths)
        
            result = result.Include(includePath);
        

        return result;
    

    /// <remarks>
    /// Adapted from https://***.com/a/49597502/1636276
    /// </remarks>
    private static IEnumerable<string> GetIncludePaths<T>(DbContext context, int maxDepth = int.MaxValue)
    
        if (maxDepth < 0)
            throw new ArgumentOutOfRangeException(nameof(maxDepth));

        var entityType = context.Model.FindEntityType(typeof(T));
        var includedNavigations = new HashSet<INavigation>();
        var stack = new Stack<IEnumerator<INavigation>>();

        while (true)
        
            var entityNavigations = new List<INavigation>();

            if (stack.Count <= maxDepth)
            
                foreach (var navigation in entityType.GetNavigations())
                
                    if (includedNavigations.Add(navigation))
                        entityNavigations.Add(navigation);
                
            

            if (entityNavigations.Count == 0)
            
                if (stack.Count > 0)
                    yield return string.Join(".", stack.Reverse().Select(e => e.Current!.Name));
            
            else
            
                foreach (var navigation in entityNavigations)
                
                    var inverseNavigation = navigation.FindInverse();
                    if (inverseNavigation != null)
                        includedNavigations.Add(inverseNavigation);
                

                stack.Push(entityNavigations.GetEnumerator());
            

            while (stack.Count > 0 && !stack.Peek().MoveNext())
                stack.Pop();

            if (stack.Count == 0)
                break;

            entityType = stack.Peek().Current!.GetTargetType();
        
    

【讨论】:

【参考方案3】:

使用.Include("Order.Customer.Address");

支持:.NET Core 3.1.8+ 当然,但我不知道是否更早

【讨论】:

请在回答前仔细阅读问题。 “所以不能使用包含...”

以上是关于Entity Framework Core 2.0.1 急切加载所有嵌套的相关实体的主要内容,如果未能解决你的问题,请参考以下文章

在 Entity Framework Core 2.0 中执行存储过程

与 Entity Framework Core 2.0 的一对零关系

Entity Framework Core 2.0:如何配置一次抽象基类

Entity Framework Core 2.0 Add-Migration 不会创建任何迁移文件

如何告诉 Entity Framework 我的 ID 列是自动递增的(AspNet Core 2.0 + PostgreSQL)?

如何防止 Entity Framework Core 2.0 重命名生成类中的表和列(数据库优先)