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)以正确地解决递归问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容