EF 6迁移至EF Core:如何重构Include表达式以支持ThenInclude?
将基于Entity Framework的.NET Framework项目迁移至.NET 8时,EF 6中通过表达式数组传递Include语句的方式在EF Core中抛出异常。
EF 6中的Include表达式数组示例:
Expression<Func<Company, object>>[] getCompaniesIncludeExpressions = { e => e.Categories.Select(c => c.Projects.Select(d => d.Jobs.Select(f => f.Location))), e => e.CompanyUsers };
通用查询方法实现:
public async Task<List<TEntity>> SelectAllAsync(Expression<Func<TEntity, object>>[] includeExpressions, Expression<Func<TEntity, bool>> whereExpression) { var entities = Context.Set<TEntity>().AsQueryable(); if (includeExpressions != null) { foreach (var includeExpression in includeExpressions) { entities = entities.Include(includeExpression); } } if (whereExpression == null) return await entities.ToListAsync(); var list = await entities .Where(whereExpression).ToListAsync(); return list; }
EF Core运行时抛出的异常信息:
AggregateException: One or more errors occurred. (The expression 'e.Categories.AsQueryable().Select(c => c.Projects.AsQueryable().Select(d => d.Jobs.AsQueryable().Select(f => f.Location)))' is invalid inside an 'Include' operation, since it does not represent a property access: 't => t.MyProperty'. To target navigations declared on derived types, use casting ('t => ((Derived)t).MyProperty') or the 'as' operator ('t => (t as Derived).MyProperty'). Collection navigation access can be filtered by composing Where, OrderBy(Descending), ThenBy(Descending), Skip or Take operations.
需重构代码以支持传递表达式并通过ThenInclude包含子属性。
方案1:用Func<IQueryable<TEntity>, IQueryable<TEntity>>封装Include链式调用
这是最贴合EF Core设计的方案,直接将Include和ThenInclude的逻辑封装为委托:
1. 修改通用查询方法
更新SelectAllAsync,接收包含Include逻辑的委托数组:
public async Task<List<TEntity>> SelectAllAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>>[] includeActions, Expression<Func<TEntity, bool>> whereExpression) { var entities = Context.Set<TEntity>().AsQueryable(); if (includeActions != null) { foreach (var includeAction in includeActions) { entities = includeAction(entities); } } if (whereExpression != null) { entities = entities.Where(whereExpression); } return await entities.ToListAsync(); }
2. 定义包含多层导航的委托数组
针对Company实体,编写完整的Include+ThenInclude逻辑:
Func<IQueryable<Company>, IQueryable<Company>>[] getCompaniesIncludeActions = { query => query.Include(c => c.Categories) .ThenInclude(cat => cat.Projects) .ThenInclude(proj => proj.Jobs) .ThenInclude(job => job.Location), query => query.Include(c => c.CompanyUsers) };
3. 调用通用方法
var companies = await SelectAllAsync(getCompaniesIncludeActions, null);
方案2:过渡方案——使用字符串导航路径
如果需要最小化修改原有代码结构,可以改用字符串形式的导航路径(注意:硬编码字符串易出错,仅作过渡用):
1. 修改通用方法接收字符串数组
public async Task<List<TEntity>> SelectAllAsync(string[] includePaths, Expression<Func<TEntity, bool>> whereExpression) { var entities = Context.Set<TEntity>().AsQueryable(); if (includePaths != null) { foreach (var path in includePaths) { entities = entities.Include(path); } } if (whereExpression != null) { entities = entities.Where(whereExpression); } return await entities.ToListAsync(); }
2. 调用时传入导航路径
string[] getCompaniesIncludePaths = { "Categories.Projects.Jobs.Location", "CompanyUsers" }; var companies = await SelectAllAsync(getCompaniesIncludePaths, null);
核心差异说明
EF Core的Include要求表达式必须是直接的属性访问,不允许通过Select嵌套表示多层包含——这是和EF6的核心区别。EF6支持用Select嵌套简化多层包含写法,但EF Core强制要求通过ThenInclude明确链式调用下一级导航属性,以此保证查询逻辑的清晰度和执行效率。
内容的提问来源于stack exchange,提问作者ledragon

