Automapper ProjectTo добавляет ToList в дочерние свойства - PullRequest
0 голосов
/ 07 сентября 2018

Я использую проекцию для сопоставления классов Entity с DTO, используя Entity Framework Core.Однако проекция добавляет 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();

Проверка сгенерированного запроса с использованием свойства Expression после ProjectTo дает следующее:

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

Этот вызов ToList заставляет запускать запросы на выборку для каждой сущности, а это не то, что я хочу, как выдогадался.Я проверил запрос без этого ToList (вручную скопировав выражение и запустив его), и все работает как положено.Как я могу предотвратить добавление этого вызова AutoMapper?Я пытался изменить тип List в DTO на IEnumerable, но ничего не изменилось ..

1 Ответ

0 голосов
/ 07 сентября 2018

Давайте проигнорируем влияние EF Core на вызов ToList и сконцентрируемся на AutoMapper ProjectTo.

Поведение жестко закодировано в EnumerableExpressionBinder классе:

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

Этот класс является частью конвейера обработки AutoMapper QueryableExtensions и отвечает за преобразование перечислимого источника в перечислимое назначение. И, как мы видим, он всегда излучает либо ToArray, либо ToList.

Фактически, когда типом элемента назначения является ICollection<T> или IList<T>, требуется вызов ToList, потому что в противном случае выражение не скомпилируется. Но когда типом элемента назначения является IEnumerable<T>, это произвольно.

Таким образом, если вы хотите избавиться от этого поведения в вышеупомянутом сценарии, вы можете ввести пользовательский IExpressionBinder до * EnumerableExpressionBinder (связыватели вызываются по порядку, пока 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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...