AutoMapper 表达式映射(作为数据源使用)
AutoMapper 在单独的 包 中支持从一个对象到另一个对象的表达式映射。这是通过将源类的属性替换为它们在目标类中所映射的属性来实现的。
假设存在以下示例类:
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; }
}
public class OrderLineDTO
{
public int Id { get; set; }
public int OrderId { get; set; }
public string Item { get; set; }
public decimal Quantity { get; set; }
}
var configuration = new MapperConfiguration(cfg =>
{
cfg.AddExpressionMapping();
cfg.CreateMap<OrderLine, OrderLineDTO>()
.ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name));
cfg.CreateMap<OrderLineDTO, OrderLine>()
.ForMember(ol => ol.Item, conf => conf.MapFrom(dto => dto));
cfg.CreateMap<OrderLineDTO, Item>()
.ForMember(i => i.Name, conf => conf.MapFrom(dto => dto.Item));
});
当从 DTO 表达式映射时
Expression<Func<OrderLineDTO, bool>> dtoExpression = dto=> dto.Item.StartsWith("A");
var expression = mapper.Map<Expression<Func<OrderLine, bool>>>(dtoExpression);
表达式将被翻译为 ol => ol.Item.Name.StartsWith("A")
AutoMapper 知道 dto.Item
映射到 ol.Item.Name
,因此它在表达式中进行了替换。
表达式映射也适用于集合的表达式。
Expression<Func<IQueryable<OrderLineDTO>,IQueryable<OrderLineDTO>>> dtoExpression = dtos => dtos.Where(dto => dto.Quantity > 5).OrderBy(dto => dto.Quantity);
var expression = mapper.Map<Expression<Func<IQueryable<OrderLine>,IQueryable<OrderLine>>>(dtoExpression);
结果为 ols => ols.Where(ol => ol.Quantity > 5).OrderBy(ol => ol.Quantity)
扁平化属性到导航属性的映射
AutoMapper 还支持将扁平化(TModel 或 DTO)属性在表达式中映射到其相应的(TData)导航属性(当导航属性已从视图模型或 DTO 中移除时),例如 CourseModel.DepartmentName
从模型表达式变为 Course.Department
在数据表达式中。
考虑以下类集:
public class CourseModel
{
public int CourseID { get; set; }
public int DepartmentID { get; set; }
public string DepartmentName { get; set; }
}
public class Course
{
public int CourseID { get; set; }
public int DepartmentID { get; set; }
public Department Department { get; set; }
}
public class Department
{
public int DepartmentID { get; set; }
public string Name { get; set; }
}
然后将 exp 映射到 expMapped。
Expression<Func<IQueryable<CourseModel>, IIncludableQueryable<CourseModel, object>>> exp = i => i.Include(s => s.DepartmentName);
Expression<Func<IQueryable<Course>, IIncludableQueryable<Course, object>>> expMapped = mapper.MapExpressionAsInclude<Expression<Func<IQueryable<Course>, IIncludableQueryable<Course, object>>>>(exp);
映射后的表达式(expMapped.ToString())是 i => i.Include(s => s.Department);
。此功能允许根据视图模型单独定义查询的导航属性。
支持的映射选项
就像 Queryable 扩展只能支持 LINQ 提供程序支持的某些内容一样,表达式翻译也遵循相同规则,即它可以和不能支持的内容。
作为数据源使用
映射表达式彼此之间是一项繁琐的工作,并且会产生冗长且丑陋的代码。
UseAsDataSource().For<DTO>()
通过不需要显式映射表达式,使这种翻译变得清晰。它还会为您调用 ProjectTo<TDO>()
(适用时)。
以 EntityFramework 为例
dataContext.OrderLines.UseAsDataSource().For<OrderLineDTO>().Where(dto => dto.Name.StartsWith("A"))
相当于
dataContext.OrderLines.Where(ol => ol.Item.Name.StartsWith("A")).ProjectTo<OrderLineDTO>()
当未调用 ProjectTo() 时
表达式翻译适用于所有类型的函数,包括 Select
调用。如果在 UseAsDataSource()
后使用 Select
并更改返回类型,则不会调用 ProjectTo<>()
,而是使用 mapper.Map
。
示例:
dataContext.OrderLines.UseAsDataSource().For<OrderLineDTO>().Select(dto => dto.Name)
相当于
dataContext.OrderLines.Select(ol => ol.Item.Name)
注册回调,用于当 UseAsDataSource() 查询被枚举时
有时,您可能想在将映射查询的结果传递给下一个应用层之前编辑集合。使用 .ProjectTo<TDto>
这很容易,因为直接返回 IQueryable<TDto>
没有意义,因为你无法再对其进行编辑。因此,你可能会这样做:
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())
{
var dtos = context.OrderLines.Where(ol => ol.OrderId == orderId)
.ProjectTo<OrderLineDTO>().ToList();
foreach(var dto in dtos)
{
// edit some property, or load additional data from the database and augment the dtos
}
return dtos;
}
}
然而,如果您使用 .UseAsDataSource()
方法,您会失去它的所有优势——特别是它在被枚举之前修改内部表达式的能力。为了解决这个问题,我们引入了 .OnEnumerated
回调。
使用它,您可以这样做:
var configuration = new MapperConfiguration(cfg =>
cfg.CreateMap<OrderLine, OrderLineDTO>()
.ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name)));
public IQueryable<OrderLineDTO> GetLinesForOrder(int orderId)
{
using (var context = new orderEntities())
{
return context.OrderLines.Where(ol => ol.OrderId == orderId)
.UseAsDataSource()
.For<OrderLineDTO>()
.OnEnumerated((dtos) =>
{
foreach(var dto in dtosCast<OrderLineDTO>())
{
// 编辑某个属性,或从数据库加载额外数据并增强dtos
}
}
}
}
当 IQueryable<OrderLineDTO>
自身被枚举时,此 OnEnumerated(IEnumerable)
回调会被执行。因此,这同样适用于上述提到的 OData 示例:OData 的 $filter 和 $orderby 表达式仍会被转换为 SQL,而 OnEnumerated()
回调会接收到从数据库中筛选并排序后的结果集。