16.AutoMapper 之可查询扩展(Queryable Extensions)

可查询扩展(Queryable Extensions)

当在像NHibernate或者Entity Framework之类的ORM框架中使用AutoMapper的标准方法Mapper.Map 时,您可能会注意到,当AutoMapper尝试将结果映射到目标类型时,ORM将查询图形中所有对象的所有字段。

如果你的ORM表达式是IQueryable的,你可以使用AutoMapperQueryableExtensions帮助方法去解决这个痛点。

Entity Framework为例,比如说你有一个实体OrderLine,它的成员Item与另外一个实体有关联。如果你想用ItemName属性将它映射到OrderLneDTO,标准的Mapper.Map调用将导致实体框架查询整个OrderLineItem表。

使用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 });

ProjectUsingConvertUsing限制略多,因为只有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)以正确地解决递归问题。

推荐阅读更多精彩内容