使用GroupBy/Select时Mapster ProjectToType报错,求解决方法
解决Mapster ProjectToType在GroupBy/Select后报EmptyProjectionMember的问题
错误原因
这个错误是因为EF Core执行GroupBy后再Select(x => x.First())的查询,会生成特殊的表达式树结构,Mapster在解析该结构时无法正确识别投影成员,导致抛出KeyNotFoundException: The given key 'EmptyProjectionMember' was not present in the dictionary。
解决办法
方法1:先查询实体到内存,再映射到DTO
将EF查询提前执行(用ToListAsync),把数据拉到内存后,再用Mapster的Adapt方法映射到DTO,绕开EF查询阶段的表达式解析问题:
public async Task<IList<T>> GetDocuments<T>(string? slug, string? languageId) { var iq = _dataContext.Documents .Where(x => slug == null || x.Slug == slug); if (languageId != null) { // 先从数据库获取实体列表 var documents = await iq .GroupBy(x => x.Slug) .Select(x => x .OrderByDescending(y => y.LanguageId == languageId) .ThenByDescending(y => y.LanguageId == "da") .ThenBy(y => y.Name) .First()) .AsNoTracking() .ToListAsync(); // 内存中映射到DTO return documents.Adapt<IList<T>>(); } else { return await iq .OrderBy(x => x.Name) .ThenBy(x => x.LanguageId) .AsNoTracking() .ProjectToType<T>() .ToListAsync(); } }
方法2:用窗口函数替代GroupBy+First,保持可解析的IQueryable
使用EF Core的窗口函数RowNumber()来实现每个Slug下的优先级筛选,这样查询返回的仍然是IQueryable<Document>,Mapster可以正常解析投影:
public async Task<IList<T>> GetDocuments<T>(string? slug, string? languageId) { var iq = _dataContext.Documents .Where(x => slug == null || x.Slug == slug); if (languageId != null) { // 用RowNumber窗口函数标记每个Slug下文档的优先级 iq = from doc in iq select new { Doc = doc, Rank = EF.Functions.RowNumber() .Over( PartitionBy: doc.Slug, OrderBy: doc.LanguageId == languageId ? 0 : 1, // 目标语言优先级最高 ThenBy: doc.LanguageId == "da" ? 0 : 1, // 丹麦语次之 ThenBy: doc.Name ) } .Where(x => x.Rank == 1) // 只取每个Slug下优先级第一的文档 .Select(x => x.Doc); } else { iq = iq.OrderBy(x => x.Name).ThenBy(x => x.LanguageId); } return await iq.AsNoTracking().ProjectToType<T>().ToListAsync(); }
关键说明
- 方法1适合数据量不大的场景,实现简单,但会把符合条件的GroupBy结果全部拉到内存。
- 方法2性能更优,所有筛选逻辑都在数据库端执行,返回的数据量更少,推荐在数据量较大时使用。
内容的提问来源于stack exchange,提问作者Mads
相关产品推荐
相关产品推荐

