.NET Core 2.1升级.NET6后出现LINQ查询翻译错误
问题复现
项目从.NET Core 2.1升级至.NET 6后,两段连续LINQ查询执行时抛出翻译错误:
- 第一段查询(执行成功,返回内存集合
studentCourses):
var studentCourses = _context.StudentAcademicPathCourses .Include(x => x.Course) .Where(x => x.StudentAcademicPathId == studentAcademicPath.Id) .Select(apc => new { apc.AcademicPathSectionId, apc.CourseDerivation, Course = new { apc.Course.EctsCredits, }, StudentCourseSection = apc.StudentCourseSection == null || false == validStudentCourseSectionIds.Contains(apc.StudentCourseSectionId) ? null : new { apc.StudentCourseSection.Ects, apc.StudentCourseSection.IsPassed }, }) .ToList();
- 第二段查询(执行时抛出异常):
var result = _context.StudentAcademicPaths .Where(x => x.Id == studentAcademicPath.Id && x.TenantId == GetRequestUserTenantId()) .Select(sap => new { AcademicPath = new { Sections = sap.AcademicPath.AcademicPathSections.Select(sec => new AcademicPathAPI_Section { CompletedECTS = studentCourses .Where(x => x.AcademicPathSectionId == sec.Id && ( x.CourseDerivation == AcademicPathCourseDerivation.Transferred || ( x.StudentCourseSection != null && true == x.StudentCourseSection.IsPassed ) ) ) .Sum(x => x.Course.EctsCredits), }) .OrderBy(x => x.Code) .ToList(), }, }).First();
抛出的异常信息:
The LINQ expression 'a => a.AcademicPathSectionId == EntityShaperExpression:
EvolveAPI.Models.AcademicPathSection
ValueBufferExpression:
ProjectionBindingExpression: EmptyProjectionMember
IsNullable: False
.Id' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'.
根本原因
.NET Core 2.1配套的EF Core 2.x版本默认支持隐式客户端求值:当查询中存在EF Core无法翻译为SQL的逻辑时,会自动把需要的数据集拉到客户端内存,用LINQ to Objects执行后续计算。
从EF Core 3.0开始(.NET 6配套的是EF Core 6),这个隐式行为被彻底移除:所有写在数据库查询表达式树中的逻辑必须能被翻译为SQL,否则直接抛出翻译异常。
当前第二段查询本质是构建SQL查询数据库,但表达式内部直接引用了已经加载到内存的studentCourses集合做关联计算,EF Core 6无法将这种内存集合与数据库表的关联逻辑翻译成SQL,也不会自动切换到客户端执行,因此触发报错。
修复方案
方案1:显式切换到客户端求值(改动最小)
先把第二段查询中需要的数据库基础数据查询到内存,再调用AsEnumerable()切换到LINQ to Objects上下文,执行关联内存集合的计算逻辑,代码示例:
var result = _context.StudentAcademicPaths .Where(x => x.Id == studentAcademicPath.Id && x.TenantId == GetRequestUserTenantId()) .Select(sap => new { AcademicPath = new { // 先查询Section基础字段,排序后拉到内存 Sections = sap.AcademicPath.AcademicPathSections .OrderBy(x => x.Code) .Select(sec => new { sec.Id, sec.Code }) .ToList() } }) .AsEnumerable() // 显式标记后续逻辑在内存中执行 .Select(sap => new { AcademicPath = new { Sections = sap.AcademicPath.Sections.Select(sec => new AcademicPathAPI_Section { Code = sec.Code, // 内存中关联studentCourses计算学分,不会触发SQL翻译 CompletedECTS = studentCourses .Where(x => x.AcademicPathSectionId == sec.Id && ( x.CourseDerivation == AcademicPathCourseDerivation.Transferred || ( x.StudentCourseSection != null && x.StudentCourseSection.IsPassed ) ) ) .Sum(x => x.Course.EctsCredits) }).ToList() } }).First();
方案2:合并为单条数据库查询(性能最优)
去掉studentCourses的单独查询,把学分计算逻辑直接合并到第二段EF Core查询中,让EF Core一次性生成关联查询SQL在数据库端完成计算,避免内存与数据库查询混合,适合数据量较大的场景。
方案3:内存数据预分组优化
如果必须使用客户端计算,可以提前把studentCourses按AcademicPathSectionId分组转成字典,避免每次遍历Section都全量扫描studentCourses集合,进一步提升内存计算性能:
// 提前按SectionId分组预计算已完成学分 var sectionCompletedEcts = studentCourses .Where(x => x.CourseDerivation == AcademicPathCourseDerivation.Transferred || (x.StudentCourseSection != null && x.StudentCourseSection.IsPassed)) .GroupBy(x => x.AcademicPathSectionId) .ToDictionary(g => g.Key, g => g.Sum(x => x.Course.EctsCredits));
后续计算CompletedECTS时直接从字典取值即可,不需要重复做Where筛选。
内容的提问来源于stack exchange,提问作者Erdogan Alper

