繁体   English   中英

Automapper ProjectTo 将 ToList 添加到子属性中

[英]Automapper ProjectTo adds ToList into child properties

我使用投影将实体类映射到使用 Entity Framework Core 的 DTO。 但是,投影将 ToList 添加到子集合属性中,这会大大减慢查询速度。

公司实体:

public class Company
{
    public Company()
    {
        Employees = new List<CompanyEmployee>();
    }

    public string Address { get; set; }
    public virtual ICollection<CompanyEmployee> Employees { get; set; }
    ...
}

公司 DTO:

public class CompanyDTO
{
    public CompanyDTO()
    {
        CompanyEmployees = new List<EmployeeDTO>();
    }

    public string Address { get; set; }
    public List<EmployeeDTO> CompanyEmployees { get; set; }
    ...
}

配置:

CreateMap<Company, CompanyDTO>()
    .ForMember(c => c.CompanyEmployees, a => a.MapFrom(src => src.Employees));
CreateMap<CompanyEmployee, EmployeeDTO>();

询问:

UnitOfWork.Repository<Company>()
    .ProjectTo<CompanyDTO>(AutoMapper.Mapper.Configuration)
    .Take(10)
    .ToList();

ProjectTo之后使用Expression属性检查生成的查询会产生以下结果:

Company.AsNoTracking()
    .Select(dtoCompany => new CompanyDTO() 
    {
        Address = dtoCompany.Address, 
        ...
        CompanyEmployees = dtoCompany.Employees.Select(dtoCompanyEmployee => new EmployeeDTO() 
                            {
                                CreatedDate = dtoCompanyEmployee.CreatedDate, 
                                ...
                            }).ToList() // WHY??????
    })

ToList调用导致为每个实体运行选择查询,这不是我想要的,正如您所猜测的那样。 我在没有ToList情况下测试了查询(通过手动复制表达式并运行它),一切都按预期工作。 如何防止 AutoMapper 添加该调用? 我尝试将 DTO 中的List类型更改为IEnumerable但没有任何改变。

让我们忽略ToList调用的EF Core影响,并专注于AutoMapper ProjectTo

该行为在EnumerableExpressionBinder类中进行了硬编码:

expression = Expression.Call(typeof(Enumerable), propertyMap.DestinationPropertyType.IsArray ? "ToArray" : "ToList", new[] { destinationListType }, expression);

此类是AutoMapper QueryableExtensions处理管道的一部分,负责将源可枚举转换为目标可枚举。 正如我们所看到的,它总是发出ToArrayToList

实际上,当目标成员类型是ICollection<T>IList<T> ,需要ToList调用,否则表达式将无法编译。 但是当目标成员类型是IEnumerable<T> ,这是任意的。

因此,如果你想在上述场景中摆脱这种行为,你可以在EnumerableExpressionBinder 之前注入一个自定义的IExpressionBinder (按顺序调用绑定器,直到IsMatch返回true ),如下所示(

namespace AutoMapper
{
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    using AutoMapper.Configuration.Internal;
    using AutoMapper.Mappers.Internal;
    using AutoMapper.QueryableExtensions;
    using AutoMapper.QueryableExtensions.Impl;

    public class GenericEnumerableExpressionBinder : IExpressionBinder
    {
        public bool IsMatch(PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionResolutionResult result) =>
            propertyMap.DestinationPropertyType.IsGenericType &&
            propertyMap.DestinationPropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>) &&
            PrimitiveHelper.IsEnumerableType(propertyMap.SourceType);

        public MemberAssignment Build(IConfigurationProvider configuration, PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary<ExpressionRequest, int> typePairCount, LetPropertyMaps letPropertyMaps)
            => BindEnumerableExpression(configuration, propertyMap, request, result, typePairCount, letPropertyMaps);

        private static MemberAssignment BindEnumerableExpression(IConfigurationProvider configuration, PropertyMap propertyMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary<ExpressionRequest, int> typePairCount, LetPropertyMaps letPropertyMaps)
        {
            var expression = result.ResolutionExpression;

            if (propertyMap.DestinationPropertyType != expression.Type)
            {
                var destinationListType = ElementTypeHelper.GetElementType(propertyMap.DestinationPropertyType);
                var sourceListType = ElementTypeHelper.GetElementType(propertyMap.SourceType);
                var listTypePair = new ExpressionRequest(sourceListType, destinationListType, request.MembersToExpand, request);
                var transformedExpressions = configuration.ExpressionBuilder.CreateMapExpression(listTypePair, typePairCount, letPropertyMaps.New());
                if (transformedExpressions == null) return null;
                expression = transformedExpressions.Aggregate(expression, (source, lambda) => Select(source, lambda));
            }

            return Expression.Bind(propertyMap.DestinationProperty, expression);
        }

        private static Expression Select(Expression source, LambdaExpression lambda)
        {
            return Expression.Call(typeof(Enumerable), "Select", new[] { lambda.Parameters[0].Type, lambda.ReturnType }, source, lambda);
        }

        public static void InsertTo(List<IExpressionBinder> binders) =>
            binders.Insert(binders.FindIndex(b => b is EnumerableExpressionBinder), new GenericEnumerableExpressionBinder());
    }
}

它基本上是EnumerableExpressionBinder的修改副本,具有不同的IsMatch检查并删除了ToList调用发出代码。

现在,如果您将其注入AutoMapper配置:

Mapper.Initialize(cfg =>
{
    GenericEnumerableExpressionBinder.InsertTo(cfg.Advanced.QueryableBinders);
    // ...
});

并使你的DTO集合类型IEnumerable<T>

public IEnumerable<EmployeeDTO> CompanyEmployees { get; set; }

ProjectTo将生成带有Select但没有ToList表达式。

不确定,但也许这个(官方) AutoMapper.EF6扩展会有所帮助。 正如你在底部看到的,它实现了ProjectToQueryable方法:

public static IQueryable<TDestination> ProjectToQueryable<TDestination>(this IQueryable queryable, object parameters)
  {
    return queryable.ProjectTo<TDestination>(parameters).DecompileAsync();
  }

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM