美文网首页.NETdotNET
16.AutoMapper 之可查询扩展(Queryable E

16.AutoMapper 之可查询扩展(Queryable E

作者: 做人生的CEO | 来源:发表于2019-01-12 00:16 被阅读4次

    可查询扩展(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)以正确地解决递归问题。

    相关文章

      网友评论

        本文标题:16.AutoMapper 之可查询扩展(Queryable E

        本文链接:https://www.haomeiwen.com/subject/wkqgrqtx.html