项目
版本

AutoMapper 可查询扩展

在使用 NHibernateEntity FrameworkORM 时,配合 AutoMapper 的标准 mapper.Map 功能,你可能会发现当 AutoMapper 尝试将查询结果映射到目标类型时,ORM 会查询整个对象图中所有对象的所有字段。

如果 ORM 提供了 IQueryable 接口,你可以利用 AutoMapperQueryableExtensions 辅助方法来解决这一痛点。

以 Entity Framework 为例,假设你有一个与 Item 实体有关联的 OrderLine 实体。如果你想将其映射到包含 ItemName 属性的 OrderLineDTO ,标准的 mapper.Map 调用会导致 Entity Framework 查询整个 OrderLineItem 表。

改用如下方法:

给定以下实体:

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:

var configuration = new MapperConfiguration(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>(configuration)
                                 .ToList();
    }
}

.ProjectTo<OrderLineDTO>() 会告诉 AutoMapper 的映射引擎向 IQueryable 发出一个 select 子句,通知 Entity Framework 只需从 Item 表中查询 Name 列,这与手动将 IQueryable 投影到包含 Select 子句的 OrderLineDTO 相同。

查询提供程序限制

ProjectTo 必须是 LINQ 方法链中的最后一个调用。ORM 处理实体而非 DTO。在实体上应用任何过滤和排序,最后一步再投影到 DTO 上。查询提供程序非常复杂,让 ProjectTo 调用放在最后可以确保查询提供程序尽可能按设计来构建针对底层查询目标(如 SQL、Mongo QL 等)的有效查询。

请注意,要使此功能工作,所有类型转换必须在你的映射中明确处理。例如,你不能依赖 Item 类的 ToString() 重写来告知 Entity Framework 仅从 Name 列中选择,任何数据类型变化,如 DoubleDecimal 也必须明确处理。

实例 API

自 8.0 版本起,当你使用 DI 和 IMapper 时,存在类似 ProjectTo 的方法,感觉更自然。

防止 懒加载/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()
};

通过 AutoMapper 的这种映射将导致 SELECT N+1 问题,除非通过 ORM 指定急切抓取,否则每个子 Course 都会被单独查询。使用 LINQ 投影,不需要对 ORM 进行特殊配置或指定。ORM 使用 LINQ 投影来构建所需的精确 SQL 查询。

这意味着使用 ProjectTo 时,你不需要使用显式急切加载(Include)。如果你需要像筛选 Include 这样的东西,可以在映射中添加过滤器:

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

自定义投影

如果成员名称不匹配,或者你想创建计算属性,可以使用 MapFrom(基于表达式的重载)为目的地成员提供自定义表达式:

var configuration = new MapperConfiguration(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 投影中执行类似操作,使用 ConvertUsing 方法:

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

基于表达式的 ConvertUsing 比基于 Func 的 ConvertUsing 重载稍微有限制,因为只有表达式和底层 LINQ 提供程序允许的内容才会工作。

自定义目标类型构造函数

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

cfg.CreateMap<Source, Dest>()
    .ConstructUsing(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>(configuration).ToList();
orders[0].OrderType.ShouldBe("Online");

显式展开

在某些场景下,比如 OData,一个泛型 DTO 通过 IQueryable 控制器动作返回。没有明确指示,AutoMapper 会展开结果中的所有成员。为了控制投影过程中哪些成员被展开,可以在配置中设置ExplicitExpansion,然后传入你想要显式展开的成员:

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));

更多信息,请参考 测试代码

聚合操作

LINQ 支持聚合查询,而 AutoMapper 也支持 LINQ 扩展方法。在自定义投影示例中,如果我们把 TotalContacts 属性重命名为 ContactsCount,AutoMapper 就会匹配到 Count() 扩展方法,而 LINQ 提供程序会将计数转化为相关子查询来聚合子记录。

如果 LINQ 提供程序支持,AutoMapper 还能支持复杂的聚合和嵌套限制:

cfg.CreateProjection<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.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} });

递归模型

理想情况下,应避免使用自我引用的模型(可做进一步研究)。但如果你必须这样做,你需要启用它们:

configuration.Internal().RecursiveQueriesMaxDepth = someRandomNumber;

支持的映射选项

并非所有映射选项都受支持,因为生成的表达式必须被 LINQ 提供程序解释。AutoMapper 只支持 LINQ 提供程序所支持的功能:

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

不支持:

  • Condition
  • SetMappingOrder
  • UseDestinationValue
  • MapFrom(基于 Func 的)
  • Before/AfterMap
  • 自定义解析器
  • 自定义类型转换器
  • ForPath
  • 值转换器
  • 领域对象上的任何计算属性
在本文档中