Search code examples
c#linq.net-coreautomapper

How to project from `Dictionary<A, B>` to `Dictionary<A,C>` with AutoMapper?


I have a fairly simple AutoMapper setup where I want to map IDictionary<Guid, From> to Dictionary<Guid, To>. This works fine with IMapper.Map, but what I really want to do is use IQueryable.ProjectTo. However, this causes the following test failure:

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

Adding the requested mapping causes all tests to fail:

    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

How can I get the projection to work?

EDIT

@LucianBargaoanu asked if "[...] that [is] even possible in EF Core without client evaluation?". Therefore I added the following test case which passes just fine.

    [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());
    }

Solution

  • ProjectTo works with server evaluation and doesn't consider dictionaries in any way (and apparently EF Core doesn't support them either).

    But what you can do is fetch what you need with ProjectTo and then Map it to a dictionary.