可查询扩展(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)以正确地解决递归问题。