EF Core 8中用DbSet.Include嵌套Select替代IncludeThen的实现问题
问题背景
我正在实现抽象泛型仓储,希望子类通过向GetById()传入Expression<Func<T, object>>列表来管理多层级关联查询,比如直接传入二级属性表达式而非手动调用.IncludeThen()。在EF 6里可以用x.Users.Select(u => u.Posts)实现嵌套关联,但EF Core 8中会抛出异常:
The expression 'x.Users.AsQueryable().Select(u => u.Posts)' is invalid inside an 'Include' operation, since it does not represent a property access: 't => t.MyProperty'
我尝试过从表达式x => x.Users.Select(u => u.Posts)中提取"Users.Posts"字符串传入Include(),但没成功。
现有代码如下:
抽象仓储的GetById方法
protected T? GetById(int id, params Expression<Func<T, object>>[] includes) => Entities .IncludeAll(includes) .FirstOrDefault(x => x.Id == id);
IncludeAll扩展方法
public static IQueryable<T> IncludeAll<T>(this IQueryable<T> entities, Expression<Func<T, object>>[]? includes) where T : Entity, new() { if (includes is null) { return entities; } entities = includes.Aggregate(entities, (current, include) => current.Include(include)); return entities; }
具体仓储实现
public override Topic? GetById(int id) { return base.GetById(id, x => x.Users.Select(u => u.Posts)); }
实体关系:Topic包含List<User> Users属性,User包含List<Post> Posts属性。
异常原因
EF Core和EF 6的Include表达式解析逻辑存在核心差异:
- EF 6允许用
Select表达式表示嵌套关联,会自动将其解析为多级包含逻辑 - EF Core要求
Include的表达式必须是直接的属性访问路径(格式为t => t.Property),不允许包含Select这类投影操作。因为EF Core的Include校验逻辑会严格检查表达式类型,x.Users.Select(u => u.Posts)属于投影操作,不符合它的规则,因此抛出异常。
解决方案
方案一:调整参数类型,适配EF Core的ThenInclude
直接修改抽象仓储的参数类型,用委托封装Include和ThenInclude的调用链,完全贴合EF Core的原生API设计:
- 定义通用委托类型
public delegate IQueryable<T> IncludeExpression<T>(IQueryable<T> query);
- 修改抽象仓储的
GetById方法
protected T? GetById(int id, params IncludeExpression<T>[] includeExpressions) { var query = Entities.AsQueryable(); foreach (var includeExpr in includeExpressions) { query = includeExpr(query); } return query.FirstOrDefault(x => x.Id == id); }
- 具体仓储调用示例
public override Topic? GetById(int id) { return base.GetById(id, query => query.Include(t => t.Users).ThenInclude(u => u.Posts) ); }
这种方式无需额外解析表达式,兼容性和可维护性最优,是EF Core推荐的多级包含写法。
方案二:解析表达式生成路径字符串
如果坚持要保留Expression<Func<T, object>>参数形式,可以编写表达式解析逻辑,将x.Users.Select(u => u.Posts)转换为"Users.Posts"这类路径字符串,再传入Include(string path)重载:
修改IncludeAll扩展方法及添加解析辅助方法:
public static IQueryable<T> IncludeAll<T>(this IQueryable<T> entities, Expression<Func<T, object>>[]? includes) where T : Entity, new() { if (includes is null || includes.Length == 0) { return entities; } foreach (var include in includes) { var path = GetIncludePath(include); if (!string.IsNullOrEmpty(path)) { entities = entities.Include(path); } } return entities; } // 解析表达式生成包含路径的辅助方法 private static string GetIncludePath<T>(Expression<Func<T, object>> expression) { var pathSegments = new List<string>(); var currentExpr = expression.Body; // 处理装箱转换(比如值类型转object) if (currentExpr is UnaryExpression unaryExpr && unaryExpr.NodeType == ExpressionType.Convert) { currentExpr = unaryExpr.Operand; } // 解析Select表达式链 while (currentExpr is MethodCallExpression methodCall && methodCall.Method.Name == "Select") { // 提取当前层级的属性名(比如Users) if (methodCall.Arguments[0] is MemberExpression sourceMember) { pathSegments.Add(sourceMember.Member.Name); } // 切换到下一层级的表达式(比如u => u.Posts) if (methodCall.Arguments[1] is LambdaExpression selectLambda) { currentExpr = selectLambda.Body; } else { break; } } // 提取最后一层属性名(比如Posts) if (currentExpr is MemberExpression finalMember) { pathSegments.Add(finalMember.Member.Name); } return string.Join(".", pathSegments); }
修改后,原有的具体仓储调用代码无需改动,x => x.Users.Select(u => u.Posts)会被自动解析为"Users.Posts"并传入Include方法生效。
注意:上述解析方法为简化实现,仅支持单层
Select嵌套。如果需要支持三级及以上关联,可将解析逻辑改为递归形式。
内容的提问来源于stack exchange,提问作者yaroslav-hub

