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
相关产品推荐
相关产品推荐

