ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

AutoMapper 投影(Projection)完全指南:用 ProjectTo 把查询翻译成高效的 SQL 后端【免费下载链接】AutoMapperA 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 N1 等经典性能陷阱。什么是投影超越扁平化AutoMapper 的常规映射mapper.Map是在内存中按名字匹配进行对象到对象的赋值。默认情况下AutoMapper 要求目标类型与源类型在命名结构上对齐这就是所谓扁平化Flattening——例如源对象的Customer.Contact.Name可以被自动映射到目标的CustomerContactName。而投影更进一步当你希望把源值投射到一个与源结构不完全一致的目标结构时需要显式指定自定义成员映射。一个典型场景是把领域模型转换成更适合网页输入表单的视图模型。例如源实体CalendarEventpublic 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.CreateMapCalendarEvent, 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.MapCalendarEvent, 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的表达式可以是任意FuncTSource, objectlambda。为什么需要 ProjectToORM 全表查询之痛用 ORMNHibernate、Entity Framework 等搭配 AutoMapper 标准的mapper.Map时你可能会注意到当 AutoMapper 尝试把结果映射到目标类型时ORM 会把对象图里所有对象的所有字段全部查询出来。如果 ORM 暴露了IQueryable就可以使用 AutoMapper 的 QueryableExtensions 辅助方法来解决这个痛点。以 Entity Framework 为例假设实体OrderLine与实体Item存在关联我们想把Item的Name属性映射到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.CreateProjectionOrderLine, OrderLineDTO() .ForMember(dto dto.Item, conf conf.MapFrom(ol ol.Item.Name)), loggerFactory); public ListOrderLineDTO GetLinesForOrder(int orderId) { using (var context new orderEntities()) { return context.OrderLines.Where(ol ol.OrderId orderId) .ProjectToOrderLineDTO(configuration).ToList(); } }底层调用链ProjectTo 是如何工作的从源码结构看ProjectTo是一组定义在 src/AutoMapper/QueryableExtensions/Extensions.cs 中的扩展方法。核心流程是ProjectToTDestination(this IQueryable source, IConfigurationProvider configuration, ...)将表达式形式的展开成员ExpressionFuncTDestination, object[]通过MemberVisitor.GetMemberPath提取为成员路径MemberPath字符串形式的展开成员则通过ReflectionHelper.GetMemberPath(typeof(TDestination), memberName)解析见 Extensions.cs最终调用configuration.Internal().ProjectionBuilder.GetProjection(source.ElementType, destinationType, parameters, memberPathsToExpand...)拿到投影表达式再用Queryable.Select把它链到源查询上见 Extensions.cs。在 src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs 中可以看到ProjectionBuilder用LockingConcurrentDictionaryProjectionRequest, QueryExpressions缓存已构建的投影——同一个源/目标类型组合的投影只计算一次并缓存后续调用直接复用因此不用担心每次查询都重建表达式树的开销。查询提供者的限制ProjectTo 必须是链上最后一个调用ProjectTo必须是 LINQ 方法链中的最后一个调用。ORM 处理的是实体而不是 DTO因此应当先对实体做过滤和排序最后一步再投影成 DTO。查询提供者非常复杂把ProjectTo放在最后可以保证查询提供者尽可能按设计方式工作生成针对底层查询目标SQL、Mongo QL 等的有效查询。另一个关键限制是该特性要求所有类型转换都必须在映射中显式处理。例如不能依赖Item类的ToString()重写来告知 EF 只从Name列取值任何数据类型变化如Double转Decimal也必须显式处理。从源码看投影表达式的生成有严格的成员解析路径ProjectionBuilder在处理每个PropertyMap时会先判断CanResolveValue、CanBeSet并跳过与构造函数参数重名的目标成员见 ProjectionBuilder.cs。当某个成员无法生成投影表达式时会抛出AutoMapperMappingException提示Error building queryable mapping strategy。实例 API通过 IMapper 使用 ProjectTo自 AutoMapper 8.0 起IMapper接口上提供了类似的ProjectTo方法在使用依赖注入DI的场景下更自然。在 src/AutoMapper/Mapper.cs 中可以看到接口签名IQueryableTDestination ProjectToTDestination(IQueryable source, object parameters null, params ExpressionFuncTDestination, object[] membersToExpand)IQueryableTDestination ProjectToTDestination(IQueryable source, IDictionarystring, object parameters, params string[] membersToExpand)IQueryable ProjectTo(IQueryable source, Type destinationType, IDictionarystring, object parameters null, params string[] membersToExpand)实现内部直接转发到source.ProjectTo(ConfigurationProvider, ...)见 Mapper.cs因此通过 DI 拿到IMapper后即可直接调用无需再显式传入IConfigurationProvider。防止懒加载与 SELECT N1 问题由 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 N1 问题——每个子Course都要单独查询一次除非通过 ORM 显式指定急切加载。而使用 LINQ 投影不需要对 ORM 做任何特殊配置或声明ORM 会依据 LINQ 投影构建出精确的 SQL 查询。也就是说使用ProjectTo时不需要显式急切加载Include。如果你需要类似过滤后的Include效果把过滤条件放进映射即可CreateProjectionEntity, Dto().ForMember(d d.Collection, o o.MapFrom(s s.Collection.Where(i ...));自定义投影用 MapFrom 构造计算属性当成员名对不上或者你想创建计算属性时可以使用MapFrom的表达式重载为目标成员提供自定义表达式var configuration new MapperConfiguration(cfg cfg.CreateProjectionCustomer, 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 投影中做同样的事同样使用ConvertUsingcfg.CreateProjectionSource, Dest().ConvertUsing(src new Dest { Value 10 });基于表达式的ConvertUsing比基于 Func 的ConvertUsing重载更受限因为只有表达式Expression所允许、且底层 LINQ 提供者能支持的内容才会生效。从源码看ConvertUsing生成的自定义映射表达式CustomMapExpression在投影构建时被优先处理——在 ProjectionBuilder.cs 的CreateProjectionCore中会先检查typeMap.CustomMapExpression若存在则直接ReplaceParameters后作为整个投影返回不再逐成员构建。自定义目标类型构造函数ConstructUsing如果目标类型有自定义构造函数但你又不想覆盖整个映射可以使用ConstructUsing的表达式重载cfg.CreateProjectionSource, 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.ProjectToOrderDto(configuration).ToList(); orders[0].OrderType.ShouldEqual(Online);该行为由投影 mapper 中的 StringProjectionMapper.cs 实现——它在源类型不是字符串、目标类型是字符串时对源表达式调用ToString()从而让 SQL 生成CAST或CONVERT之类的转换。显式展开Explicit Expansion按需控制投影成员在某些场景下例如 OData一个通用 DTO 会通过返回IQueryable的控制器 action 暴露给外部。如果没有显式指示AutoMapper 会展开结果中的所有成员。要控制投影期间展开哪些成员在配置中设置ExplicitExpansion然后传入你想要显式展开的成员// 配置标记需要显式展开的成员 cfg.CreateProjectionSource, Dest() .ForMember(m m.Child1, opt opt.ExplicitExpansion()) .ForMember(m m.Child2, opt opt.ExplicitExpansion()); // 使用只显式展开 Child2表达式形式 dbContext.Orders.ProjectToOrderDto(configuration, dest dest.Customer, dest dest.LineItems); // 或字符串形式 dbContext.Orders.ProjectToOrderDto(configuration, null, Customer, LineItems); // 集合成员的嵌套展开 dbContext.Orders.ProjectToOrderDto(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属性改名为ContactsCountAutoMapper 会自动匹配到Count()扩展方法LINQ 提供者会把该计数翻译成关联子查询来聚合子记录。AutoMapper 还支持更复杂的聚合和嵌套限制只要 LINQ 提供者支持cfg.CreateProjectionCourse, 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.CreateProjectionCourse, CourseModel() .ForMember(m m.CurrentUserName, opt opt.MapFrom(src currentUserName));投影时在运行时替换参数dbContext.Courses.ProjectToCourseModel(Config, new { currentUserName Request.User.Name });其原理是先捕获原表达式中闭包字段的名字然后在查询发送给查询提供者之前用匿名对象/字典把值应用到参数上。也可以使用字典来构建投影值dbContext.Courses.ProjectToCourseModel(Config, new Dictionarystring, 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验证了父节点的父节点为 nullresult[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基于表达式IgnoreNullSubstitute值转换器Value transformersIncludeMembers使用Include/IncludeBase的运行时多态映射不支持的选项ConditionSetMappingOrderUseDestinationValueMapFrom基于 FuncBeforeMap/AfterMap自定义解析器Custom resolvers自定义类型转换器Custom type convertersForPath值转换器Value converters领域对象上的任何计算属性Any calculated property最后一条值得特别强调由于投影最终要翻译成底层查询提供者能执行的表达式领域实体上的计算属性仅存在于类中、无法翻译为 SQL 的 getter不会被投影。这也是文档不支持列表与查询提供者限制一节的共同结论——能用MapFrom表达式表达的就写在映射里依赖领域对象自身逻辑的只能在映射后处理。总结AutoMapper 的投影体系由两条主线组成配置侧的CreateProjection配合MapFrom、ConvertUsing、ConstructUsing、ExplicitExpansion等成员选项负责声明目标长什么样执行侧的ProjectToIConfigurationProvider扩展或IMapper实例方法负责把声明翻译成查询提供者可执行的表达式。理解 ProjectionBuilder.cs 的构建与缓存机制、RecursiveQueriesMaxDepth与PolymorphicProjectionsEnabled两个全局开关以及所有类型转换必须显式处理、ProjectTo必须是链上最后一环这两条纪律就能在实际项目中写出既高效又可被 ORM 正确翻译的投影查询。相关延伸阅读Queryable Extensions 文档ProjectTo 的姊妹篇聚焦查询扩展方法的原理与限制Extensions.csProjectTo全部重载的源码实现ProjectionBuilder.cs投影表达式构建、缓存、参数化、递归与多态的核心实现ExplicitExpansion.cs 与 RecursiveQuery.cs显式展开与递归深度的行为验证测试赞分享后端【免费下载链接】AutoMapperA convention-based object-object mapper in .NET.项目地址https://gitcode.com/gh_mirrors/au/AutoMapper点击查看免费下载相关推荐如何使用SQL翻译器让AI帮你轻松生成SQL查询的完整指南如何使用SQL翻译器让AI帮你轻松生成SQL查询的完整指南 SQL Translator是一款使用人工智能将自然语言查询转换为SQL代码的免费开源工具。它能帮AI 应用前端FOLib/folib查询结果投影Projection与数据字段过滤策略FOLib/folib查询结果投影Projection与数据字段过滤策略 在AI研发过程中数据查询往往返回大量冗余字段不仅浪费网络带宽还会增加数据处理的后端供应链安全包管理器开发者工具5个常见Love Iwara使用问题解决方案视频播放、下载与登录故障排查5个常见Love Iwara使用问题解决方案视频播放、下载与登录故障排查 Love Iwara是一款功能强大的跨平台第三方Iwara客户端应用支持Andro移动开发桌面应用音视频视频上一篇创维E900V22D刷入Armbian一步到位改造成Linux服务节点下一篇用 Calibre 搞定繁简转换TradSimpChinese 插件完全指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表