Automapper ProjectTo 将 ToList 添加到子属性中 [英] Automapper ProjectTo adds ToList into child properties
问题描述
我使用投影将实体类映射到使用 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 处理管道的一部分,负责将源可枚举转换为目标可枚举.正如我们所见,它总是发出 ToArray
或 ToList
.
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
.
事实上,当目标成员类型为ICollection
或IList
时,需要调用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屋!