Automapper ProjectTo 将 ToList 添加到子属性中 [英] Automapper ProjectTo adds ToList into child properties

查看:23
本文介绍了Automapper ProjectTo 将 ToList 添加到子属性中的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I use projection to map the Entity classes to DTOs using Entity Framework Core. However, projection adds ToList into child collection properties and this slows down the query a lot.

公司实体:

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 属性检查生成的查询会产生以下结果:

Inspecting the generated query using the Expression property after ProjectTo yields the following:

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 但没有任何改变..

That ToList call causes to run select queries for each entity which is not what I want as you have guessed. I tested the query without that ToList (by manually copying the expression and running it) and everything works as expected. How can I prevent AutoMapper adding that call? I tried changing List type in DTO to IEnumerable but nothing changed..

推荐答案

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

Let ignore the EF Core affect of the ToList call and concentrate on AutoMapper ProjectTo.

行为被硬编码在 <代码>EnumerableExpressionBinder 类:

The behavior is hardcoded in EnumerableExpressionBinder class:

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

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

This class in part of the AutoMapper QueryableExtensions processing pipeline and is responsible for converting source enumerable to destination enumerable. And as we can see, it always emits either ToArray or ToList.

事实上,当目标成员类型为ICollectionIList时,需要调用ToList,否则表达式不会编译.但是当目标成员类型是 IEnumerable 时,这是任意的.

In fact when the destination member type is ICollection<T> or IList<T>, the ToList call is needed because otherwise the expression won't compile. But when the destination member type is IEnumerable<T>, this is arbitrary.

因此,如果您想摆脱上述场景中的这种行为,您可以在 EnumerableExpressionBinder(绑定器按顺序调用,直到 IsMatch 返回 true) 像这样 (

So if you want to get rid of that behavior in the aforementioned scenario, you can inject a custom IExpressionBinder before the EnumerableExpressionBinder (the binders are called in order until IsMatch returns true) like this (

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 调用发出代码.

It's basically a modified copy of the EnumerableExpressionBinder with different IsMatch check and removed ToList call emit code.

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

Now if you inject it to your AutoMapper configuration:

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

并使您的 DTO 集合类型IEnumerable:

and make your DTO collection type IEnumerable<T>:

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

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

the ProjectTo will generate expression with Select but w/o ToList.

这篇关于Automapper ProjectTo 将 ToList 添加到子属性中的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆