You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

EF Core执行Include时自动为ICollection属性执行ThenInclude的方法

问题描述

现有如下C#业务实现代码:

public async Task<T?> SingleOrDefaultClientWhereAndSelect<T>(
    Expression<Func<Client, bool>> where, 
    Expression<Func<Client, T>> select,
    List<Expression<Func<Client, object>>> includes
)
{
    if (where is null) throw new ArgumentNullException(nameof(where), "where was null");
    if (select is null) throw new ArgumentNullException(nameof(select), "select was null");

    var query = _configurationDbContext.Clients.Where(where);

    foreach (var include in includes)
    {
        query.Include(include);
    }

    return await query.Select(select).SingleOrDefaultAsync();
}

需求:在foreach循环中调用Include方法加载关联数据时,需要在未知被Include对象具体类型的前提下,自动加载该对象关联的所有ICollection类型集合导航属性,通过ThenInclude方法完成级联加载。

实现方案

首先需要修复原代码的逻辑问题:EF Core的Include/ThenInclude属于不可变操作,调用后会返回新的IQueryable实例,原代码直接调用query.Include(include)未将返回值回写给query变量,会导致所有Include配置完全不生效。

核心实现思路如下:

  • 借助EF Core内置的模型元数据能力,在运行时识别任意实体类型的导航属性,不需要提前知道被Include对象的具体类型
  • 对每个传入的第一层Include表达式,解析出其指向的最终实体类型
  • 递归遍历该实体类型的所有导航属性,筛选出ICollection<>类型的集合导航,动态构造ThenInclude表达式完成级联加载
  • 增加最大递归深度限制,避免双向导航属性引发无限循环

完整实现代码如下:

// 根据业务场景调整最大递归深度,避免双向导航导致死循环
private const int MaxCascadeIncludeDepth = 3;

public async Task<T?> SingleOrDefaultClientWhereAndSelect<T>(
    Expression<Func<Client, bool>> where, 
    Expression<Func<Client, T>> select,
    List<Expression<Func<Client, object>>> includes)
{
    if (where is null) throw new ArgumentNullException(nameof(where), "where was null");
    if (select is null) throw new ArgumentNullException(nameof(select), "select was null");

    IQueryable<Client> query = _configurationDbContext.Clients.Where(where);
    var efModel = _configurationDbContext.Model;

    foreach (var includeExpr in includes)
    {
        // 应用第一层Include,必须接住返回值
        var currentQuery = query.Include(includeExpr);
        // 解析当前Include路径对应的终点实体类型
        var currentEntityType = ResolveIncludeEndType(includeExpr, efModel);
        // 递归追加所有集合导航的ThenInclude配置
        currentQuery = AppendCollectionThenIncludes(currentQuery, currentEntityType, efModel, 1);
        query = currentQuery;
    }

    return await query.Select(select).SingleOrDefaultAsync();
}

/// <summary>
/// 解析Include表达式最终指向的实体类型
/// </summary>
private IEntityType ResolveIncludeEndType(LambdaExpression includeExpression, IModel efModel)
{
    var exprBody = includeExpression.Body;
    // 处理导航属性为值类型时被装箱为object产生的Convert节点
    if (exprBody is UnaryExpression unaryExpr && unaryExpr.NodeType == ExpressionType.Convert)
    {
        exprBody = unaryExpr.Operand;
    }

    if (exprBody is MemberExpression memberExpr)
    {
        var memberClrType = memberExpr.Type;
        // 集合类型取泛型参数作为实际实体类型
        if (memberClrType.IsGenericType && memberClrType.GetGenericTypeDefinition() == typeof(ICollection<>))
        {
            memberClrType = memberClrType.GetGenericArguments()[0];
        }

        return efModel.FindEntityType(memberClrType)
            ?? throw new InvalidOperationException($"类型{memberClrType.FullName}未注册为EF Core实体");
    }

    throw new ArgumentException($"不支持的Include表达式格式: {includeExpression}");
}

/// <summary>
/// 递归为当前实体追加所有ICollection导航的ThenInclude配置
/// </summary>
private IQueryable<TSource> AppendCollectionThenIncludes<TSource>(
    IQueryable<TSource> query,
    IEntityType currentEntityType,
    IModel efModel,
    int currentDepth)
{
    if (currentDepth >= MaxCascadeIncludeDepth) return query;

    // 筛选当前实体下所有ICollection类型的集合导航
    var collectionNavs = currentEntityType.GetNavigations()
        .Where(nav => 
            nav.IsCollection
            && nav.ClrType.IsGenericType
            && nav.ClrType.GetGenericTypeDefinition() == typeof(ICollection<>))
        .ToList();

    foreach (var nav in collectionNavs)
    {
        // 动态构造ThenInclude需要的lambda表达式
        var param = Expression.Parameter(currentEntityType.ClrType, "e");
        var propAccess = Expression.Property(param, nav.PropertyInfo!);
        var thenIncludeLambda = Expression.Lambda(propAccess, param);

        // 反射调用ThenInclude方法,处理未知导航类型的场景
        var thenIncludeMethod = typeof(EntityFrameworkQueryableExtensions)
            .GetMethods()
            .First(m => 
                m.Name == nameof(EntityFrameworkQueryableExtensions.ThenInclude)
                && m.GetParameters().Length == 2
                && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(IIncludableQueryable<,>))
            .MakeGenericMethod(
                typeof(TSource),
                currentEntityType.ClrType,
                nav.ClrType.GetGenericArguments()[0]);

        var afterThenInclude = (IQueryable<TSource>)thenIncludeMethod.Invoke(null, new object[] { query, thenIncludeLambda })!;
        
        // 递归处理下一层级的集合导航
        var nextEntityType = efModel.FindEntityType(nav.ClrType.GetGenericArguments()[0])!;
        query = AppendCollectionThenIncludes(afterThenInclude, nextEntityType, efModel, currentDepth + 1);
    }

    return query;
}

补充说明

  • 代码默认最大递归深度为3层,可根据业务实际需要调整MaxCascadeIncludeDepth常量值,避免双向导航(如A包含B集合、B包含A集合)引发无限递归
  • 由于场景下无法提前知道被Include导航的具体类型,代码通过反射调用泛型ThenInclude方法,这是EF Core动态加载场景的标准实现方式
  • 导航属性识别完全依赖EF Core的模型元数据,不需要为实体额外加特性或维护导航关系配置,只要实体和导航属性在DbContext中正确配置即可自动识别
  • 所有Include/ThenInclude的返回值都正确回写,避免原代码中Include不生效的问题

内容的提问来源于stack exchange,提问作者Szyszka947

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 19:27:23