16.AutoMapper 之可查询扩展(Queryable Extensions)
可查询扩展(Queryable Extensions)
当在像NHibernate
或者Entity Framework
之类的ORM
框架中使用AutoMapper的标准方法Mapper.Map
时,您可能会注意到,当AutoMapper
尝试将结果映射到目标类型时,ORM
将查询图形中所有对象的所有字段。
如果你的ORM
表达式是IQueryable
的,你可以使用AutoMapper
的QueryableExtensions
帮助方法去解决这个痛点。
以Entity Framework
为例,比如说你有一个实体OrderLine
,它的成员Item
与另外一个实体有关联。如果你想用Item
的Name
属性将它映射到OrderLneDTO
,标准的Mapper.Map
调用将导致实体框架查询整个OrderLine
和Item
表。
使用QueryableExtensions
帮助方法代替。
相关实体:
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; }
}
相关DTO
public class OrderLineDTO
{
public int Id { get; set; }
public int OrderId { get; set; }
public string Item { get; set; }
public decimal Quantity { get; set; }
}
你可以像这样使用Queryable Extensions
:
Mapper.Initialize(cfg =>
cfg.CreateMap<OrderLine, OrderLineDTO>()
.ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name)));
public List<OrderLineDTO> GetLinesForOrder(int orderId)
{
using (var context = new orderEntities())
{
return context.OrderLines.Where(ol => ol.OrderId == orderId)
.ProjectTo<OrderLineDTO>().ToList();
}
}
.ProjectTo <OrderLineDTO>()
将告诉AutoMapper的映射引擎向IQueryable发出一个select
子句,该子句将通知实体框架它只需要查询Item表的Name列,就像用Select
子句手动将IQueryable
投影到OrderLineDTO
一样。
请注意,要使此功能起作用,必须在Mapping中显式处理所有类型转换。举个例子,你不能通过重写Item
类的ToString()
方法来告诉实体框架只查询Name
列,并且必须明确处理数据类型转换,例如“Double”转“Decimal”。
防止延迟加载/SELECT N+1 问题
因为AutoMapper构建的LINQ投影通过查询提供器直接转换为SQL查询,映射发生在SQL/ADO.NET级别,并没有涉及到你的实体。所以所有数据都被加载到你的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()
};
以上例子将导致SELECT N + 1问题,因为每个子成员Course
都将执行一次查询,除非通过ORM指定立即获取。使用LINQ投影,ORM不需要特殊配置或规范。ORM使用LINQ投影来构建所需的确切SQL查询。
自定义投影
如果成员名称不对应,或者您想要创建计算属性,则可以使用MapFrom(而不是ResolveUsing)为目标成员提供自定义表达式:
Mapper.Initialize(cfg => cfg.CreateMap<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()));
AutoMapper使用构建的投影传递提供的表达式. 只要您的查询提供器可以解析提供的表达式,所有内容都将一直传递到数据库。
如果表达式被您的查询提供器(Entity Framework,NHibernate等)拒绝,您可能需要调整表达式,直到找到一个被接受的表达式。
自定义类型转换
有时,你需要完全替换源类型到目标类型的类型转换。在正常的运行时映射中,通过ConvertUsing方法完成。要在LINQ投影中达到类似的目的,请使用ProjectUsing方法:
cfg.CreateMap<Source, Dest>().ProjectUsing(src => new Dest { Value = 10 });
ProjectUsing
比ConvertUsing
限制略多,因为只有Expression中允许的内容和底层LINQ提供器支持的才有效。
自定义目标类型构造函数
如果你的目标类型有自定义的构造器,但你又不想重写整个映射,那么久使用ConstructProjectionUsing方法:
cfg.CreateMap<Source, Dest>()
.ConstructProjectionUsing(src => new Dest(src.Value + 10));
AutoMapper将根据匹配的名称自动将目标构造函数参数与源成员匹配,因此,如果AutoMapper无法正确匹配目标构造函数,或者在构造期间需要扩展定义,则只能使用此方法。
字符串转换
当目标成员类型是字符串而源成员类型不是时,AutoMapper将自动添加ToString()
。
public class Order {
public OrderTypeEnum OrderType { get; set; }
}
public class OrderDto {
public string OrderType { get; set; }
}
var orders = dbContext.Orders.ProjectTo<OrderDto>().ToList();
orders[0].OrderType.ShouldEqual("Online");
显式展开
在某些情况下,例如OData,通过IQueryable控制器操作返回的通用DTO。如果没有明确的说明,AutoMapper将展开结果中的所有成员。为了在投影期间控制哪些成员要被展开,在配置中设置ExplicitExpansion然后后传入要显式展开的成员中去。
dbContext.Orders.ProjectTo<OrderDto>(
dest => dest.Customer,
dest => dest.LineItems);
// 或者基于字符串类型的
dbContext.Orders.ProjectTo<OrderDto>(
null,
"Customer",
"LineItems");
聚合
LINQ可以支持聚合查询,AutoMapper又支持LINQ扩展方法。在自定义投影的例子中,如果我们将TotalContacts
成员重命名为ContactsCount
,AutoMapper 将匹配Count()
扩展方面并且LINQ提供器将计数转换为相关子查询以聚合子记录。
如果LINQ提供程序支持,AutoMapper还可以支持复杂的聚合和嵌套限制:
cfg.CreateMap<Course, CourseModel>()
.ForMember(m => m.EnrollmentsStartingWithA,
opt => opt.MapFrom(c => c.Enrollments.Where(e => e.Student.LastName.StartsWith("A")).Count()));
此查询返回每个课程姓氏以字母“A”开头的学生总数。
参数化
有时候,投影需要运行时的参数做为它的值。如果需要将当前用户名作为它数据的一部分时,可以使用参数化MapFrom配置,来代替使用映射后代码:
string currentUserName = null;
cfg.CreateMap<Course, CourseModel>()
.ForMember(m => m.CurrentUserName, opt => opt.MapFrom(src => currentUserName));
当我们投影时,我们将在运行时替换我们的参数:
dbContext.Courses.ProjectTo<CourseModel>(Config, new { currentUserName = Request.User.Name });
这将通过捕获原始表达式中闭包的字段名称来实现,然后使用匿名对象/字典在将查询发送给查询提供器之前将值应用于参数值。
支持的映射选项
不是所有映射选项都被支持,因为生成的表达式最终由LINQ提供器来解析。所以只有被LINQ提供器支持的才会被AutoMapper支持:
- MapFrom
- Ignore
- UseValue
- NullSubstitute
不支持的:
- Condition
- DoNotUseDestinationValue
- SetMappingOrder
- UseDestinationValue
- ResolveUsing
- Before/AfterMap
- 自定义解析器
- 自定义类型转换器
- 在程序域对象上的任何计算属性
另外,递归或自引用目标类型不被LINQ提供器支持,所以也不被支持。典型的层次关系数据模型需要公共表表达式参与(CTEs)以正确地解决递归问题。