news 2026/9/24 17:05:27

AutoMapper 投影(Projection)完全指南:用 ProjectTo 把查询翻译成高效的 SQL

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AutoMapper 投影(Projection)完全指南:用 ProjectTo 把查询翻译成高效的 SQL
  • 后端

【免费下载链接】AutoMapper

A convention-based object-object mapper in .NET.

项目地址:https://gitcode.com/gh_mirrors/au/AutoMapper
点击查看免费下载

投影(Projection)是 AutoMapper 中把源对象结构"重塑"为目标结构的能力——它不只是扁平化对象模型,而是允许在映射时执行自定义成员映射、聚合、参数化表达式,并配合ProjectTo将 LINQ 查询直接翻译成底层查询提供者(Entity Framework、NHibernate、Mongo 等)认识的表达式树,让 ORM 只查询真正需要的列。读完本文,你将掌握CreateProjection的完整配置语法、ProjectTo的多种调用形态、显式展开与参数化等进阶用法,并能依据源码理解投影表达式的构建与缓存机制,避免 SELECT N+1 等经典性能陷阱。

什么是投影:超越扁平化

AutoMapper 的常规映射(mapper.Map)是在内存中"按名字匹配"进行对象到对象的赋值。默认情况下,AutoMapper 要求目标类型与源类型在命名结构上对齐,这就是所谓扁平化(Flattening)——例如源对象的Customer.Contact.Name可以被自动映射到目标的CustomerContactName

投影更进一步:当你希望把源值投射到一个与源结构不完全一致的目标结构时,需要显式指定自定义成员映射。一个典型场景是把领域模型转换成更适合网页输入表单的视图模型。

例如,源实体CalendarEvent

public class CalendarEvent { public DateTime Date { get; set; } public string Title { get; set; } }

需要转换成按"年/月/日拆分"的输入表单模型:

public class CalendarEventForm { public DateTime EventDate { get; set; } public int EventHour { get; set; } public int EventMinute { get; set; } public string Title { get; set; } }

因为目标属性名与源属性名并不精确对应(CalendarEvent.Date需要变成CalendarEventForm.EventDate),我们需要在类型映射配置中给出自定义成员映射:

// 源数据 var calendarEvent = new CalendarEvent { Date = new DateTime(2008, 12, 15, 20, 30, 0), Title = "Company Holiday Party" }; // 配置 AutoMapper var configuration = new MapperConfiguration(cfg => cfg.CreateMap<CalendarEvent, CalendarEventForm>() .ForMember(dest => dest.EventDate, opt => opt.MapFrom(src => src.Date.Date)) .ForMember(dest => dest.EventHour, opt => opt.MapFrom(src => src.Date.Hour)) .ForMember(dest => dest.EventMinute, opt => opt.MapFrom(src => src.Date.Minute)), loggerFactory); // 执行映射 CalendarEventForm form = mapper.Map<CalendarEvent, CalendarEventForm>(calendarEvent); form.EventDate.ShouldEqual(new DateTime(2008, 12, 15)); form.EventHour.ShouldEqual(20); form.EventMinute.ShouldEqual(30); form.Title.ShouldEqual("Company Holiday Party");

每个自定义成员的配置都通过一个 action 委托完成。上面的例子使用了MapFrom选项来执行自定义的源到目标成员映射。MapFrom接收一个 lambda 表达式作为参数,该表达式会在稍后的映射过程中被求值。MapFrom的表达式可以是任意Func<TSource, object>lambda。

为什么需要 ProjectTo:ORM 全表查询之痛

用 ORM(NHibernate、Entity Framework 等)搭配 AutoMapper 标准的mapper.Map时,你可能会注意到:当 AutoMapper 尝试把结果映射到目标类型时,ORM 会把对象图里所有对象的所有字段全部查询出来。

如果 ORM 暴露了IQueryable,就可以使用 AutoMapper 的 QueryableExtensions 辅助方法来解决这个痛点。以 Entity Framework 为例,假设实体OrderLine与实体Item存在关联,我们想把ItemName属性映射到OrderLineDTO。使用标准mapper.Map会让 EF 把整张OrderLine表和整张Item表都查出来。

改用ProjectTo之后,AutoMapper 的映射引擎会向IQueryable发出一个select子句,告知 EF 只需要查询Item表的Name列——就像你手动用Select子句把IQueryable投影成OrderLineDTO一样。

public class OrderLine { public int Id { get; set; } public int OrderId { get; set; } public Item Item { get; set; } public decimal Quantity { get; set; } } public class Item { public int Id { get; set; } public string Name { get; set; } } public class OrderLineDTO { public int Id { get; set; } public int OrderId { get; set; } public string Item { get; set; } public decimal Quantity { get; set; } }
var configuration = new MapperConfiguration(cfg => cfg.CreateProjection<OrderLine, OrderLineDTO>() .ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name)), loggerFactory); public List<OrderLineDTO> GetLinesForOrder(int orderId) { using (var context = new orderEntities()) { return context.OrderLines.Where(ol => ol.OrderId == orderId) .ProjectTo<OrderLineDTO>(configuration).ToList(); } }

底层调用链:ProjectTo 是如何工作的

从源码结构看,ProjectTo是一组定义在 src/AutoMapper/QueryableExtensions/Extensions.cs 中的扩展方法。核心流程是:

  1. ProjectTo<TDestination>(this IQueryable source, IConfigurationProvider configuration, ...)将表达式形式的展开成员(Expression<Func<TDestination, object>>[])通过MemberVisitor.GetMemberPath提取为成员路径(MemberPath);
  2. 字符串形式的展开成员则通过ReflectionHelper.GetMemberPath(typeof(TDestination), memberName)解析(见 Extensions.cs);
  3. 最终调用configuration.Internal().ProjectionBuilder.GetProjection(source.ElementType, destinationType, parameters, memberPathsToExpand...)拿到投影表达式,再用Queryable.Select把它链到源查询上(见 Extensions.cs)。

在 src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs 中可以看到,ProjectionBuilderLockingConcurrentDictionary<ProjectionRequest, QueryExpressions>缓存已构建的投影——同一个源/目标类型组合的投影只计算一次并缓存,后续调用直接复用,因此不用担心每次查询都重建表达式树的开销。

查询提供者的限制:ProjectTo 必须是链上最后一个调用

ProjectTo必须是 LINQ 方法链中的最后一个调用。ORM 处理的是实体而不是 DTO,因此应当先对实体做过滤和排序,最后一步再投影成 DTO。查询提供者非常复杂,把ProjectTo放在最后可以保证查询提供者尽可能按设计方式工作,生成针对底层查询目标(SQL、Mongo QL 等)的有效查询。

另一个关键限制是:该特性要求所有类型转换都必须在映射中显式处理。例如,不能依赖Item类的ToString()重写来告知 EF 只从Name列取值;任何数据类型变化(如DoubleDecimal)也必须显式处理。

从源码看,投影表达式的生成有严格的成员解析路径:ProjectionBuilder在处理每个PropertyMap时,会先判断CanResolveValueCanBeSet,并跳过与构造函数参数重名的目标成员(见 ProjectionBuilder.cs)。当某个成员无法生成投影表达式时,会抛出AutoMapperMappingException,提示"Error building queryable mapping strategy"。

实例 API:通过 IMapper 使用 ProjectTo

自 AutoMapper 8.0 起,IMapper接口上提供了类似的ProjectTo方法,在使用依赖注入(DI)的场景下更自然。在 src/AutoMapper/Mapper.cs 中可以看到接口签名:

  • IQueryable<TDestination> ProjectTo<TDestination>(IQueryable source, object parameters = null, params Expression<Func<TDestination, object>>[] membersToExpand)
  • IQueryable<TDestination> ProjectTo<TDestination>(IQueryable source, IDictionary<string, object> parameters, params string[] membersToExpand)
  • IQueryable ProjectTo(IQueryable source, Type destinationType, IDictionary<string, object> parameters = null, params string[] membersToExpand)

实现内部直接转发到source.ProjectTo(ConfigurationProvider, ...)(见 Mapper.cs),因此通过 DI 拿到IMapper后即可直接调用,无需再显式传入IConfigurationProvider

防止懒加载与 SELECT N+1 问题

由 AutoMapper 构建的 LINQ 投影会由查询提供者直接翻译成 SQL 查询,映射发生在 SQL/ADO.NET 层面,根本不会触碰你的实体对象——所有数据都会被急切(eagerly)获取并加载进 DTO。

嵌套集合会使用Select来投影子 DTO,相当于以下手写查询:

from i in db.Instructors orderby i.LastName select new InstructorIndexData.InstructorModel { ID = i.ID, FirstMidName = i.FirstMidName, LastName = i.LastName, HireDate = i.HireDate, OfficeAssignmentLocation = i.OfficeAssignment.Location, Courses = i.Courses.Select(c => new InstructorIndexData.InstructorCourseModel { CourseID = c.CourseID, CourseTitle = c.Title }).ToList() };

如果用普通映射(mapper.Map)配合懒加载实现同样的效果,会出现 SELECT N+1 问题——每个子Course都要单独查询一次,除非通过 ORM 显式指定急切加载。而使用 LINQ 投影,不需要对 ORM 做任何特殊配置或声明,ORM 会依据 LINQ 投影构建出精确的 SQL 查询。

也就是说,使用ProjectTo不需要显式急切加载(Include)。如果你需要类似过滤后的Include效果,把过滤条件放进映射即可:

CreateProjection<Entity, Dto>().ForMember(d => d.Collection, o => o.MapFrom(s => s.Collection.Where(i => ...));

自定义投影:用 MapFrom 构造计算属性

当成员名对不上,或者你想创建计算属性时,可以使用MapFrom表达式重载为目标成员提供自定义表达式:

var configuration = new MapperConfiguration(cfg => cfg.CreateProjection<Customer, CustomerDto>() .ForMember(d => d.FullName, opt => opt.MapFrom(c => c.FirstName + " " + c.LastName)) .ForMember(d => d.TotalContacts, opt => opt.MapFrom(c => c.Contacts.Count())), loggerFactory);

AutoMapper 会把提供的表达式并入构建的投影中。只要你的查询提供者能解释这个表达式,一切都会被一路下推到数据库。

如果表达式被查询提供者(EF、NHibernate 等)拒绝,你可能需要不断调整表达式,直到找到被接受的写法。

自定义类型转换:投影中的 ConvertUsing

偶尔你需要完全替换某个从源类型到目标类型的转换。在常规运行时映射中,这是通过ConvertUsing方法完成的;要在 LINQ 投影中做同样的事,同样使用ConvertUsing

cfg.CreateProjection<Source, Dest>().ConvertUsing(src => new Dest { Value = 10 });

基于表达式的ConvertUsing比基于 Func 的ConvertUsing重载更受限,因为只有表达式(Expression)所允许、且底层 LINQ 提供者能支持的内容才会生效。

从源码看,ConvertUsing生成的自定义映射表达式(CustomMapExpression)在投影构建时被优先处理——在 ProjectionBuilder.cs 的CreateProjectionCore中,会先检查typeMap.CustomMapExpression,若存在则直接ReplaceParameters后作为整个投影返回,不再逐成员构建。

自定义目标类型构造函数:ConstructUsing

如果目标类型有自定义构造函数,但你又不想覆盖整个映射,可以使用ConstructUsing表达式重载

cfg.CreateProjection<Source, Dest>() .ConstructUsing(src => new Dest(src.Value + 10));

AutoMapper 会自动按名字匹配把目标构造函数参数对应到源成员上,所以只有当 AutoMapper 无法正确匹配目标构造函数,或者你在构造过程中需要额外定制时,才需要使用这个方法。投影构建时,若TypeMap带有CustomCtorExpression,会用该表达式生成NewExpression;否则会走ConstructorMap(可解析时逐参数投影),最后兜底使用无参New(typeMap.DestinationType)(见 ProjectionBuilder.cs)。

字符串转换:自动 ToString

当目标成员类型是string而源成员类型不是时,AutoMapper 会自动添加ToString()

public class Order { public OrderTypeEnum OrderType { get; set; } } public class OrderDto { public string OrderType { get; set; } } var orders = dbContext.Orders.ProjectTo<OrderDto>(configuration).ToList(); orders[0].OrderType.ShouldEqual("Online");

该行为由投影 mapper 中的 StringProjectionMapper.cs 实现——它在源类型不是字符串、目标类型是字符串时,对源表达式调用ToString(),从而让 SQL 生成CASTCONVERT之类的转换。

显式展开(Explicit Expansion):按需控制投影成员

在某些场景下(例如 OData),一个通用 DTO 会通过返回IQueryable的控制器 action 暴露给外部。如果没有显式指示,AutoMapper 会展开结果中的所有成员。要控制投影期间展开哪些成员,在配置中设置ExplicitExpansion,然后传入你想要显式展开的成员:

// 配置:标记需要显式展开的成员 cfg.CreateProjection<Source, Dest>() .ForMember(m => m.Child1, opt => opt.ExplicitExpansion()) .ForMember(m => m.Child2, opt => opt.ExplicitExpansion()); // 使用:只显式展开 Child2(表达式形式) dbContext.Orders.ProjectTo<OrderDto>(configuration, dest => dest.Customer, dest => dest.LineItems); // 或字符串形式 dbContext.Orders.ProjectTo<OrderDto>(configuration, null, "Customer", "LineItems"); // 集合成员的嵌套展开 dbContext.Orders.ProjectTo<OrderDto>(configuration, null, dest => dest.LineItems.Select(item => item.Product));

语义细节可以从测试 src/UnitTests/Projection/ExplicitExpansion.cs 中确认:被标记ExplicitExpansion()未在ProjectTo调用中列出的成员,投影结果中保持null;显式列出的成员会被展开;而没有标记ExplicitExpansion()的成员仍会默认展开(见 ExplicitExpansion.cs 的三个断言)。展开判断的核心逻辑在ProjectionRequest.ShouldExpand(见 ProjectionBuilder.cs):只有当前成员路径是某个待展开成员路径的前缀时,才执行成员投影(ShouldExpand() => memberMap.ExplicitExpansion != true || request.ShouldExpand(...),见 ProjectionBuilder.cs)。

聚合(Aggregations):把 Count 翻译成关联子查询

LINQ 支持聚合查询,AutoMapper 支持 LINQ 扩展方法。在自定义投影示例中,如果把TotalContacts属性改名为ContactsCount,AutoMapper 会自动匹配到Count()扩展方法,LINQ 提供者会把该计数翻译成关联子查询来聚合子记录。

AutoMapper 还支持更复杂的聚合和嵌套限制(只要 LINQ 提供者支持):

cfg.CreateProjection<Course, CourseModel>() .ForMember(m => m.EnrollmentsStartingWithA, opt => opt.MapFrom(c => c.Enrollments.Where(e => e.Student.LastName.StartsWith("A")).Count()));

这个查询返回每门课程中,姓氏以字母 'A' 开头的学生总数。

参数化(Parameterization):在投影中注入运行时值

有时投影需要运行时参数作为取值来源。例如,投影需要把当前用户名拉进数据。与其用映射后处理代码,不如把MapFrom配置参数化:

string currentUserName = null; cfg.CreateProjection<Course, CourseModel>() .ForMember(m => m.CurrentUserName, opt => opt.MapFrom(src => currentUserName));

投影时,在运行时替换参数:

dbContext.Courses.ProjectTo<CourseModel>(Config, new { currentUserName = Request.User.Name });

其原理是:先捕获原表达式中闭包字段的名字,然后在查询发送给查询提供者之前,用匿名对象/字典把值应用到参数上。也可以使用字典来构建投影值:

dbContext.Courses.ProjectTo<CourseModel>(Config, new Dictionary<string, object> { {"currentUserName", Request.User.Name} });

注意:使用字典会把值硬编码进查询(而非参数化查询),请谨慎使用。

从源码看,参数替换由ParameterVisitor完成(见 ProjectionBuilder.cs):它通过检测成员声明类型是否带CompilerGeneratedAttribute(即编译器生成的闭包字段)来识别参数,匿名对象走PropertyVisitor(反射取同名属性),字典走ConstantVisitor(直接把字典值作为常量替换进表达式树——这正是"硬编码"的根源)。只有未启用EnableNullPropagationForQueryMapping且未传参数时,投影才会直接命中缓存;否则每次调用都要经QueryExpressions.Prepare重新处理参数(见 ProjectionBuilder.cs)。

递归模型(Recursive Models):限制递归查询深度

理想情况下应避免引用自身的模型。但如果必须使用,需要显式启用:

configuration.Internal().RecursiveQueriesMaxDepth = someRandomNumber;

该配置项在 src/AutoMapper/Internal/InternalApi.cs 中定义。其生效逻辑位于 ProjectionBuilder.cs:当遇到递归成员请求(memberRequest.AlreadyExists)且当前深度已达到RecursiveQueriesMaxDepth时,返回null,停止继续展开。测试 src/UnitTests/Projection/RecursiveQuery.cs 将RecursiveQueriesMaxDepth设为 1,验证了"父节点的父节点为 null"(result[0].Parent.Parent.ShouldBeNull())的行为。

多态投影(Polymorphic Projection)

许多 ORM 支持继承和多态模型,包括自定义投影。例如 Entity Framework Core 使用多种映射策略支持继承。部分 LINQ 查询提供者也支持多态投影,AutoMapper 在构建Select查询时会尝试使用多态投影。

如果尝试过程中查询提供者抛出了异常,很可能是该查询提供者不支持你场景下的多态投影。此时可以关闭多态投影:

configuration.PolymorphicProjectionsEnabled = false;

关闭后,AutoMapper 不再尝试检查源类型来为 LINQ 投影挑选特定的目标类型。

该开关与多态映射的关联可以在源码中得到印证:Profile.cs 定义了bool? PolymorphicProjectionsEnabled,全局默认值在 MapperConfiguration.cs 中解析为true。在 ProjectionBuilder.cs 中,PolymorphicMaps仅在_configuration.PolymorphicProjectionsEnabled为真时,才收集IncludedDerivedTypes中的派生类型映射;构建时用Condition(TypeIs(source, sourceType), derivedProjection, projection)为每个派生源类型生成条件分支(见 ProjectionBuilder.cs),并用TypeAs把源表达式安全转型到派生类型。

支持的映射选项与不支持的选项

并非所有映射选项都能用于投影,因为生成的表达式必须能被 LINQ 提供者解释。AutoMapper 只支持 LINQ 提供者能支持的内容:

支持的选项:

  • MapFrom(基于表达式)
  • ConvertUsing(基于表达式)
  • Ignore
  • NullSubstitute
  • 值转换器(Value transformers)
  • IncludeMembers
  • 使用Include/IncludeBase的运行时多态映射

不支持的选项:

  • Condition
  • SetMappingOrder
  • UseDestinationValue
  • MapFrom(基于 Func)
  • BeforeMap/AfterMap
  • 自定义解析器(Custom resolvers)
  • 自定义类型转换器(Custom type converters)
  • ForPath
  • 值转换器(Value converters)
  • 领域对象上的任何计算属性(Any calculated property)

最后一条值得特别强调:由于投影最终要翻译成底层查询提供者能执行的表达式,领域实体上的计算属性(仅存在于类中、无法翻译为 SQL 的 getter)不会被投影。这也是文档"不支持列表"与"查询提供者限制"一节的共同结论——能用MapFrom表达式表达的,就写在映射里;依赖领域对象自身逻辑的,只能在映射后处理。

总结

AutoMapper 的投影体系由两条主线组成:配置侧CreateProjection(配合MapFromConvertUsingConstructUsingExplicitExpansion等成员选项)负责声明"目标长什么样",执行侧ProjectToIConfigurationProvider扩展或IMapper实例方法)负责把声明翻译成查询提供者可执行的表达式。理解 ProjectionBuilder.cs 的构建与缓存机制、RecursiveQueriesMaxDepthPolymorphicProjectionsEnabled两个全局开关,以及"所有类型转换必须显式处理、ProjectTo必须是链上最后一环"这两条纪律,就能在实际项目中写出既高效又可被 ORM 正确翻译的投影查询。

相关延伸阅读:

  • Queryable Extensions 文档:ProjectTo 的姊妹篇,聚焦查询扩展方法的原理与限制
  • Extensions.cs:ProjectTo全部重载的源码实现
  • ProjectionBuilder.cs:投影表达式构建、缓存、参数化、递归与多态的核心实现
  • ExplicitExpansion.cs 与 RecursiveQuery.cs:显式展开与递归深度的行为验证测试
  • 后端

【免费下载链接】AutoMapper

A convention-based object-object mapper in .NET.

项目地址:https://gitcode.com/gh_mirrors/au/AutoMapper
点击查看免费下载

相关推荐

上一篇:创维E900V22D刷入Armbian:一步到位改造成Linux服务节点
下一篇:用 Calibre 搞定繁简转换:TradSimpChinese 插件完全指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 17:02:52

大麦自动抢票指南:Python Selenium + Appium 双端抢票脚本完整教程

大麦自动抢票指南&#xff1a;Python Selenium Appium 双端抢票脚本完整教程 【免费下载链接】ticket-purchase 大麦自动抢票&#xff0c;支持人员、城市、日期场次、价格选择 项目地址: https://gitcode.com/GitHub_Trending/ti/ticket-purchase ticket-purchase 是一…

作者头像 李华
网站建设 2026/9/24 17:01:00

高速传输灵活交付 金士顿移动存储赋能项目全周期数据流转迁移

乙方项目归档交付应包含完整项目的原始素材、源文件、多版迭代稿件、最终成片与交付文档等等。而实际上&#xff0c;很多行业往往需要混合办公、跨地协作&#xff0c;依托网盘存储看似便利实际暗藏隐患&#xff0c;不仅容易出现版本错乱、链接过期、文件压缩损坏、画质音质失真…

作者头像 李华
网站建设 2026/9/24 16:57:10

yaml-cpp 安装指南:5 步从源码到跑通第一个 YAML 解析

yaml-cpp 安装指南&#xff1a;5 步从源码到跑通第一个 YAML 解析 【免费下载链接】yaml-cpp A YAML parser and emitter in C 项目地址: https://gitcode.com/GitHub_Trending/ya/yaml-cpp yaml-cpp 是一个符合 YAML 1.2 规范的 C 库&#xff0c;负责在 C 程序里解析和…

作者头像 李华
网站建设 2026/9/24 16:56:36

零售数据分析:如何用用户行为数据把“转化率“从3%提到8%?

做电商和零售的朋友&#xff0c;应该都对转化率这个词特别敏感。同样的流量&#xff0c;转化率3%和8%&#xff0c;业绩差的可不是一点半点。很多人转化率上不去&#xff0c;就知道瞎优化主图、改价格&#xff0c;折腾来折腾去&#xff0c;效果微乎其微。其实转化率不是靠感觉调…

作者头像 李华
网站建设 2026/9/24 16:54:56

Quick 入门实战:在 Xcode 项目中配置 Swift / Objective-C 单元测试

测试开发工具 【免费下载链接】Quick The Swift (and Objective-C) testing framework. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/qu/Quick 点击查看 免费下载 本篇指南围绕 Quick 测试框架的使用前置环节——在 Xcode 工程中正确搭建测试 Target 与跨语言测试桥…

作者头像 李华