EF查询中映射大量同名属性到DTO,有何优于逐属性赋值的高效方案
适用于EF场景的零冗余投影实现方案
核心前提:所有映射逻辑必须嵌入EF的查询表达式树,保证整个投影过程被翻译为纯SQL执行,不触发客户端内存求值,从根本上规避逐一声明属性赋值的冗余。
方案1:利用EF Core 5.0+ 嵌套实体自动匹配(零代码修改成本)
EF Core 5.0及以上版本的查询翻译器,已支持在投影时自动将嵌套实体的同名字段匹配到目标类型的对应属性,不需要逐一声明每个属性的赋值逻辑。
实现代码示例:
private List<PolysubByYearDto> buildDtos(IQueryable<vw_Polysub> polysubs) { var query = from p in polysubs join c in uowTr.CountyRepository.List() on p.primary_cty equals c.County_Code into lj1 from polysubWithCounty in lj1.DefaultIfEmpty() join t in uowTr.PolysubRepository.ListPolyType() on p.poly_type_cd equals t.poly_type_cd into lj2 from polysubWithType in lj2.DefaultIfEmpty() join period in uowTr.PolysubRepository.ListReportPeriods() on p.rec_no equals period.rec_no into lj3 from polysubWithPeriod in lj3.DefaultIfEmpty() // 第一阶段:构造嵌套匿名类型,EF会自动展开所有需要查询的列生成纯SQL select new { SourceBase = p, // 直接嵌入基础实体,EF会自动识别为需要查询vw_Polysub的全部映射列 ExtCountyName = polysubWithCounty == null ? string.Empty : polysubWithCounty.county_name, ExtPolyName = polysubWithType == null ? string.Empty : polysubWithType.poly_name_ds, ExtYearEndMonth = polysubWithPeriod?.year_end_month ?? 0 } // 第二阶段:构造目标Dto,EF会自动将SourceBase中的同名字段映射到Dto的继承属性,无客户端求值 .Select(proj => new PolysubByYearDto { PrimaryCountyName = proj.ExtCountyName, PolyTypeName = proj.ExtPolyName, ReportYearEndMonth = proj.ExtYearEndMonth }); return query.ToList(); }
- 工作原理:EF的查询翻译器会自动遍历Dto的所有可写属性,从前面的投影结果中按“属性名称+类型”匹配对应的值,哪怕值是嵌套在
SourceBase对象中的属性,因此完全不需要手动声明这些同名字段的赋值逻辑。 - 优势:后续如果
vw_Polysub新增字段,只要PolysubByYearDto作为派生类继承了该字段,不需要修改任何投影代码,EF会自动把新列加入查询、完成映射,完全消除冗余维护成本;该写法不会触发客户端求值,生成的SQL会直接拉取vw_Polysub的所有列加上3个关联字段,执行性能和手写全字段赋值的写法完全一致。
方案2:预生成表达式树映射(适配所有EF版本,包括老版本EF6)
如果使用的EF版本不支持嵌套实体自动匹配,可以在程序编译/启动阶段预生成好所有同名字段的映射表达式树,运行时直接嵌入查询,同样能被EF翻译为纯SQL,不会引入运行时反射开销:
- 启动阶段预生成映射逻辑,遍历两个类型的同名同类型属性,生成标准的EF可解析表达式:
// 程序启动时初始化一次,永久复用 private static readonly Expression<Func<vw_Polysub, string, string, int, PolysubByYearDto>> _dtoMapper = BuildMapperExpression(); private static Expression<Func<vw_Polysub, string, string, int, PolysubByYearDto>> BuildMapperExpression() { var pParam = Expression.Parameter(typeof(vw_Polysub), "p"); var countyNameParam = Expression.Parameter(typeof(string), "countyName"); var polyNameParam = Expression.Parameter(typeof(string), "polyName"); var yearEndMonthParam = Expression.Parameter(typeof(int), "yearEndMonth"); var bindings = new List<MemberBinding> { // 绑定3个自定义扩展属性 Expression.Bind(typeof(PolysubByYearDto).GetProperty(nameof(PolysubByYearDto.PrimaryCountyName)), countyNameParam), Expression.Bind(typeof(PolysubByYearDto).GetProperty(nameof(PolysubByYearDto.PolyTypeName)), polyNameParam), Expression.Bind(typeof(PolysubByYearDto).GetProperty(nameof(PolysubByYearDto.ReportYearEndMonth)), yearEndMonthParam) }; // 遍历vw_Polysub的公开属性,自动生成与Dto的同名同类型属性的绑定 var sourceProps = typeof(vw_Polysub).GetProperties(); var targetProps = typeof(PolysubByYearDto).GetProperties(); foreach (var sourceProp in sourceProps) { var matchProp = targetProps.FirstOrDefault(tp => tp.Name == sourceProp.Name && tp.PropertyType == sourceProp.PropertyType); if (matchProp != null) { bindings.Add(Expression.Bind(matchProp, Expression.Property(pParam, sourceProp))); } } var newDtoExpr = Expression.New(typeof(PolysubByYearDto).GetConstructor(Type.EmptyTypes)); var memberInit = Expression.MemberInit(newDtoExpr, bindings); return Expression.Lambda<Func<vw_Polysub, string, string, int, PolysubByYearDto>>( memberInit, pParam, countyNameParam, polyNameParam, yearEndMonthParam); }
- 查询逻辑直接使用预生成的表达式:
private List<PolysubByYearDto> buildDtos(IQueryable<vw_Polysub> polysubs) { var query = from p in polysubs join c in uowTr.CountyRepository.List() on p.primary_cty equals c.County_Code into lj1 from polysubWithCounty in lj1.DefaultIfEmpty() join t in uowTr.PolysubRepository.ListPolyType() on p.poly_type_cd equals t.poly_type_cd into lj2 from polysubWithType in lj2.DefaultIfEmpty() join period in uowTr.PolysubRepository.ListReportPeriods() on p.rec_no equals period.rec_no into lj3 from polysubWithPeriod in lj3.DefaultIfEmpty() select _dtoMapper.Invoke( p, polysubWithCounty == null ? string.Empty : polysubWithCounty.county_name, polysubWithType == null ? string.Empty : polysubWithType.poly_name_ds, polysubWithPeriod?.year_end_month ?? 0 ); return query.ToList(); }
- 优势:适配所有支持IQueryable翻译的EF版本,映射过程无运行时反射、无客户端求值,执行性能和手写赋值完全一致;后续新增同名字段时,只要属性类型匹配,预生成的表达式会自动把字段加入映射逻辑,不需要手动修改Select代码。
常见避坑点
- 禁止在映射流程中调用任何依赖内存对象的逻辑(比如运行时反射、AutoMapper内存映射、手动
ToList()后再赋值),这类逻辑会触发EF的客户端求值,将全表数据拉到内存后再处理,违背性能要求。 - 如果使用AutoMapper的
ProjectTo做投影,本质和方案2的预生成表达式逻辑完全一致,需要严格配置无客户端逻辑的纯投影规则,避免触发不必要的内存求值。
内容的提问来源于stack exchange,提问作者Coty Penn
相关产品推荐
相关产品推荐

