Как спроецировать из `Dictionary<A, B>` в `Dictionary<A,C>` с помощью AutoMapper?

У меня довольно простая настройка AutoMapper, в которой я хочу сопоставить IDictionary<Guid, From> с Dictionary<Guid, To>. Это отлично работает с IMapper.Map, но мне действительно хочется использовать IQueryable.ProjectTo. Однако это приводит к следующему сбою теста:

using System;
using System.Collections.Generic;
using System.Linq;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Xunit;

public class AutoMapperTests()
{
    private readonly IMapper mapper = new MapperConfiguration(
        (cfg) =>
        {
            cfg.CreateMap<From, To>();
        }
    ).CreateMapper();

    [Fact]
    public void AutoMapperConfigurationIsValid() =>
        mapper.ConfigurationProvider.AssertConfigurationIsValid();

    [Fact]
    public void CanMapFromDictToDict()
    {
        var key = Guid.NewGuid();
        var fromDict = CreateFromDict(key, key.ToString());

        var toDict = new[] { fromDict }
            .AsEnumerable()
            .Select(it => mapper.Map<IDictionary<Guid, To>>(fromDict))
            .Single();

        Assert.Single(toDict);
        Assert.True(toDict.ContainsKey(key));
        Assert.True(toDict[key].Id == key.ToString());
    }

    [Fact]
    public void CanProjectFromDictToDict()
    {
        var key = Guid.NewGuid();
        var fromDict = CreateFromDict(key, key.ToString());

        var toDict = new[] { fromDict }
            .AsQueryable()
            .ProjectTo<IDictionary<Guid, To>>(mapper.ConfigurationProvider)
            .Single();

        Assert.Single(toDict);
        Assert.True(toDict.ContainsKey(key));
        Assert.True(toDict[key].Id == key.ToString());
    }

    private IDictionary<Guid, From> CreateFromDict(Guid key, string id) =>
        new Dictionary<Guid, From>
        {
            {
                key,
                new() { Id = id }
            }
        };

    public class From
    {
        public required string Id { get; set; }
    }

    public class To
    {
        public required string Id { get; set; }
    }
}
[xUnit.net 00:00:00.15]     AutoMapperTests.CanProjectFromDictToDict [FAIL]
  Failed AutoMapperTests.CanProjectFromDictToDict [7 ms]
  Error Message:
   System.InvalidOperationException : Missing map from System.Collections.Generic.IDictionary`2[System.Guid,AutoMapperTests+From] to System.Collections.Generic.IDictionary`2[System.Guid,AutoMapperTests+To]. Create using CreateMap<IDictionary`2, IDictionary`2>.
  Stack Trace:
     at AutoMapper.QueryableExtensions.Impl.ProjectionBuilder.PolymorphicMaps(ProjectionRequest& request)
   at AutoMapper.QueryableExtensions.Impl.ProjectionBuilder.CreateProjection(ProjectionRequest request)
   at AutoMapper.Internal.LockingConcurrentDictionary`2.<>c__DisplayClass2_1.<.ctor>b__1()
   at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
   at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.get_Value()
   at AutoMapper.Internal.LockingConcurrentDictionary`2.GetOrAdd(TKey& key)
   at AutoMapper.QueryableExtensions.Impl.ProjectionBuilder.GetProjection(Type sourceType, Type destinationType, Object parameters, MemberPath[] membersToExpand)
   at AutoMapper.QueryableExtensions.Extensions.ToCore(IQueryable source, Type destinationType, IConfigurationProvider configuration, Object parameters, IEnumerable`1 memberPathsToExpand)
   at AutoMapper.QueryableExtensions.Extensions.ToCore[TResult](IQueryable source, IConfigurationProvider configuration, Object parameters, IEnumerable`1 memberPathsToExpand)
   at AutoMapper.QueryableExtensions.Extensions.ProjectTo[TDestination](IQueryable source, IConfigurationProvider configuration, Object parameters, Expression`1[] membersToExpand)
   at AutoMapper.QueryableExtensions.Extensions.ProjectTo[TDestination](IQueryable source, IConfigurationProvider configuration, Expression`1[] membersToExpand)
   at AutoMapperTests.CanProjectFromDictToDict() in /<redacted>/AutoMapperTests.cs:line 43
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

Failed!  - Failed:     1, Passed:     2, Skipped:     0, Total:     3, Duration: 21 ms

Добавление запрошенного сопоставления приводит к сбою всех тестов:

    private readonly IMapper mapper = new MapperConfiguration(
        (cfg) =>
        {
            cfg.CreateMap<From, To>();
            cfg.CreateMap<IDictionary<Guid, From>, IDictionary<Guid, To>>();
        }
    ).CreateMapper();
[xUnit.net 00:00:00.13]     AutoMapperTests.AutoMapperConfigurationIsValid [FAIL]
[xUnit.net 00:00:00.13]     AutoMapperTests.CanProjectFromDictToDict [FAIL]
[xUnit.net 00:00:00.13]     AutoMapperTests.CanMapFromDictToDict [FAIL]
  Failed AutoMapperTests.AutoMapperConfigurationIsValid [1 ms]
  Error Message:
   System.ArgumentException : Incorrect number of arguments supplied for call to method 'To get_Item(System.Guid)' (Parameter 'property')
  Stack Trace:
     at System.Linq.Expressions.Expression.Property(Expression expression, PropertyInfo property)
   at AutoMapper.Execution.TypeMapPlanBuilder.CreatePropertyMapFunc(MemberMap memberMap, Expression destination, MemberInfo destinationMember)
   at AutoMapper.Execution.TypeMapPlanBuilder.AddPropertyMaps(List`1 actions)
   at AutoMapper.Execution.TypeMapPlanBuilder.CreateAssignmentFunc(Expression createDestination)
   at AutoMapper.Execution.TypeMapPlanBuilder.CreateMapperLambda()
   at AutoMapper.TypeMap.CreateMapperLambda(IGlobalConfiguration configuration)
   at AutoMapper.TypeMap.Seal(IGlobalConfiguration configuration)
   at AutoMapper.MapperConfiguration.<.ctor>g__Seal|20_0()
   at AutoMapper.MapperConfiguration..ctor(MapperConfigurationExpression configurationExpression)
   at AutoMapper.MapperConfiguration..ctor(Action`1 configure)
   at AutoMapperTests..ctor() in /<redacted>/AutoMapperTests.cs:line 10
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean wrapExceptions)
  Failed AutoMapperTests.CanProjectFromDictToDict [1 ms]
  Error Message:
   System.ArgumentException : Incorrect number of arguments supplied for call to method 'To get_Item(System.Guid)' (Parameter 'property')
  Stack Trace:
     at System.Linq.Expressions.Expression.Property(Expression expression, PropertyInfo property)
   at AutoMapper.Execution.TypeMapPlanBuilder.CreatePropertyMapFunc(MemberMap memberMap, Expression destination, MemberInfo destinationMember)
   at AutoMapper.Execution.TypeMapPlanBuilder.AddPropertyMaps(List`1 actions)
   at AutoMapper.Execution.TypeMapPlanBuilder.CreateAssignmentFunc(Expression createDestination)
   at AutoMapper.Execution.TypeMapPlanBuilder.CreateMapperLambda()
   at AutoMapper.TypeMap.CreateMapperLambda(IGlobalConfiguration configuration)
   at AutoMapper.TypeMap.Seal(IGlobalConfiguration configuration)
   at AutoMapper.MapperConfiguration.<.ctor>g__Seal|20_0()
   at AutoMapper.MapperConfiguration..ctor(MapperConfigurationExpression configurationExpression)
   at AutoMapper.MapperConfiguration..ctor(Action`1 configure)
   at AutoMapperTests..ctor() in /<redacted>/AutoMapperTests.cs:line 10
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean wrapExceptions)
  Failed AutoMapperTests.CanMapFromDictToDict [1 ms]
  Error Message:
   System.ArgumentException : Incorrect number of arguments supplied for call to method 'To get_Item(System.Guid)' (Parameter 'property')
  Stack Trace:
     at System.Linq.Expressions.Expression.Property(Expression expression, PropertyInfo property)
   at AutoMapper.Execution.TypeMapPlanBuilder.CreatePropertyMapFunc(MemberMap memberMap, Expression destination, MemberInfo destinationMember)
   at AutoMapper.Execution.TypeMapPlanBuilder.AddPropertyMaps(List`1 actions)
   at AutoMapper.Execution.TypeMapPlanBuilder.CreateAssignmentFunc(Expression createDestination)
   at AutoMapper.Execution.TypeMapPlanBuilder.CreateMapperLambda()
   at AutoMapper.TypeMap.CreateMapperLambda(IGlobalConfiguration configuration)
   at AutoMapper.TypeMap.Seal(IGlobalConfiguration configuration)
   at AutoMapper.MapperConfiguration.<.ctor>g__Seal|20_0()
   at AutoMapper.MapperConfiguration..ctor(MapperConfigurationExpression configurationExpression)
   at AutoMapper.MapperConfiguration..ctor(Action`1 configure)
   at AutoMapperTests..ctor() in /<redacted>/AutoMapperTests.cs:line 10
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean wrapExceptions)

Failed!  - Failed:     3, Passed:     0, Skipped:     0, Total:     3, Duration: 2 ms

Как заставить проекцию работать?

РЕДАКТИРОВАТЬ

@LucianBargaoanu спросил, «[...] это вообще возможно в EF Core без оценки клиента?». Поэтому я добавил следующий тестовый пример, который проходит нормально.

    [Fact]
    public void CanProjectManuallyFromDictToDict()
    {
        var key = Guid.NewGuid();
        var fromDict = CreateFromDict(key, key.ToString());

        var toDict = new[] { fromDict }
            .AsQueryable()
            .Select(it => fromDict.ToDictionary(it => it.Key, it => new To { Id = it.Value.Id }))
            .Single();

        Assert.Single(toDict);
        Assert.True(toDict.ContainsKey(key));
        Assert.True(toDict[key].Id == key.ToString());
    }

Эта карта бесполезна :) Вам нужно реализовать ее с помощью Convertusing.

Lucian Bargaoanu 06.09.2024 05:43

Я так и думал, но не знаю, как это сделать с помощью ConvertUsing. Мои настоящие типы From и To довольно сложны и полиморфны, поэтому мне бы не хотелось снова объявлять полное ручное преобразование просто для словарной проекции. ConstructUsing обеспечивает ctx.Mapper.Map, но мне тоже не удалось заставить это работать.

Felix ZY 06.09.2024 10:13

Возможно ли это вообще в EF Core без оценки клиента? ProjectTo работает с оценкой сервера и никак не учитывает словари. Сначала попробуйте это в запросе LINQ, без AM.

Lucian Bargaoanu 06.09.2024 10:25

@LucianBargaoanu Я добавил тестовый пример для проверки. Да, похоже, это сработало.

Felix ZY 06.09.2024 11:10

Это не имеет значения, вам нужно выполнить запрос к реальной БД и доказать, что он работает с оценкой сервера. В противном случае просто используйте Map.

Lucian Bargaoanu 06.09.2024 11:13

Я получаю System.InvalidOperationException : The LINQ expression 'it => it.Key' could not be translated., что кажется немного странной ошибкой, но, возможно, Linq получит наиболее конкретный ответ?

Felix ZY 06.09.2024 11:42

Я так думаю. Что вы можете сделать, так это получить то, что вам нужно, с помощью ProjectTo, а затем Map в словарь.

Lucian Bargaoanu 06.09.2024 11:54

Я пытаюсь спроецировать свои классы сущностей на классы моделей, сохраняя при этом IQueryable, поэтому я бы предпочел не делать этого, но, думаю, от этого никуда не деться... @LucianBargaoanu Если вы напишете это как ответ, я приму это.

Felix ZY 06.09.2024 12:09
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
8
53
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

ProjectTo работает с оценкой сервера и никак не учитывает словари (и судя по всему EF Core их тоже не поддерживает).

Но что вы можете сделать, так это получить то, что вам нужно, с помощью ProjectTo, а затем Map в словарь.

Другие вопросы по теме