当使用带有AutoMapper
标准Mapper.Map
函数的ORM
(如NHibernate
或Entity Framework
)时,您可能会注意到,当AutoMapper
尝试将结果映射到目标类型时,ORM
将查询图中所有对象的所有字段。
如果你的ORM
暴露IQueryables
,你可以使用AutoMapper
的QueryableExtensions
辅助方法来解决这个关键的问题。
使用Entity Framework
作为例子,说你有一个与实体Item
有关系的实体OrderLine
。 如果要使用Item
的Name
属性将其映射到OrderLineDTO
,则标准的Mapper.Map
调用将导致Entity Framework
查询整个OrderLine
和Item
表。
改用这种方法。
鉴于以下实体
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
列,就像手动将IQueryable
投影到带有Select
子句的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()
};
通过AutoMapper
的这个映射将导致一个SELECT N+1
的问题,因为每个子成员(课程)将被一次一个地查询,除非通过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);
// or string-based
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提供者来解释。 AutoMapper
只支持LINQ提供程序支持的内容:
MapFrom
Ignore
UseValue
NullSubstitute
不支持:
Condition
DoNotUseDestinationValue
SetMappingOrder
UseDestinationValue
ResolveUsing
Before/AfterMap
Custom resolvers
Custom type converters
您的领域对象上的任何计算属性
此外,递归或自引用目标类型不支持,因为LINQ提供程序不支持这种类型。 通常,分层关系数据模型需要通用表表达式(CTE)
来正确解析递归连接。