Skip to content

ForAllMaps with CreateProjection #4206

@pinkfloydx33

Description

@pinkfloydx33

After upgrading from 12.0.0 to 12.0.1 we have been unable to even construct a MapperConfiguration from our existing profiles. I've been able to isolate the problem down to a combination of:

  1. An enum that uses ConvertUsing in combination with CreateProjection
  2. A call to CreateProjection for a class that has a property of the enum
  3. A call to ForAllMaps

The exception does not occur if you remove any one of the above items. It also does not occur if you remove the enum property from the class in 2.

See the More Details section below for an explanation.

Source/destination types

public class Source
{
    public string Name { get; set; }
    public SourceEnum Value { get; set; }
}

public class Dest
{
    public string Name { get; set; }
    public DestEnum Value { get; set; }
}

public enum SourceEnum { A, B }
public enum DestEnum   { A, B }

Mapping configuration

var cfgExp = new MapperConfigurationExpression();
cfgExp.AddProfile<ExampleProfile>();

_ = new MapperConfiguration(cfgExp); // exception thrown here, but see below

public class ExampleProfile : Profile
{
    public ExampleProfile()
    {
        CreateProjection<SourceEnum, DestEnum>() // 1; Necessary for Postgres-style Enums which are not int-based
            .ConvertUsing(
                  src => src == SourceEnum.A ? DestEnum.A : DestEnum.B
            );

        CreateProjection<Source, Dest>(); // 2

        this.Internal().ForAllMaps(static (_, _) => { });  // 3; don't need to do anything in the callback
    }
}

Version: 12.0.1

Fails on Version 12.0.1
Works on Version 12.0.0

Also using EF7 but that really doesn't matter as the exception can be reproduced without it.

Expected behavior

Does not throw an exception creating the map/configuration and can be successfully used. Works correctly in 12.0.0

Actual behavior

An Exception is thrown when constructing the mapper expression. It's impossible to use this profile to create a Mapper. The Exception occurs after the final invocation of the ForAllMaps method.

System.InvalidOperationException
HResult=0x80131509
Message=Incorrect number of arguments supplied for lambda invocation
Source=System.Linq.Expressions
StackTrace:
at System.Dynamic.Utils.ExpressionUtils.ValidateArgumentCount(MethodBase method, ExpressionType nodeKind, Int32 count, ParameterInfo[] pis)
at System.Linq.Expressions.Expression.Invoke(Expression expression, Expression arg0, Expression arg1, Expression arg2)
at System.Linq.Expressions.Expression.Invoke(Expression expression, IEnumerable`1 arguments)
at AutoMapper.TypeMap.Invoke(Expression source, Expression destination)
at AutoMapper.Execution.ExpressionBuilder.MapExpression(IGlobalConfiguration configuration, ProfileMap profileMap, TypePair typePair, Expression source, MemberMap memberMap, Expression destination)
at AutoMapper.Execution.TypeMapPlanBuilder.MapMember(MemberMap memberMap, ParameterExpression resolvedValue, Expression destinationMemberValue)
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 Program.

$(String[] args) in C:\Users\pinkfloyx33\source\repos\AutoMapperBug\AutoMapperBug\Program.cs:line 7

Note that the Exception is not thrown if you remove any of lines 1, 2, or 3 in the ExampleProfile above.

Steps to reproduce

Create MapperConfiguration from attached profile (see above)

More Details

We are using native Postgres enums in our project which is based on EFcore7. A native Postgres enum is not number-based and as such a direct projection cannot be performed (based on the name/value) and will throw an error at runtime (from npgsql). To get around this you need to create a projection for the enum types and use ConvertUsing with (nested) ternary expressions to account for all cases. This has always worked properly for us.

ForAllMaps - I realize this in the Internal namespace, so why do we use it? We have a common set of audit fields (create/modify date/user) across all of our Db and API models. Unfortunately:

  • the names aren't exact matches to each other (even accounting for prefixes) thanks to different sets of company-wide conventions
  • the date fields are DateTimeOffset in the API model and DateTime at the db-level
    • Postgres doesn't support DateTimeOffset in the classic sense (ie. time+offset) and will normalize the offset on read to the server's local time. DateTime is preferred and always tracks UTC
    • Additionally, the modified date is nullable which requires a cast in the mapping/projection expressions

Rather than specifying the two sets of member mappings/nullable casts for every map, in every profile, we have an extension method that calls ForAllMaps which in turn checks the target/destination types for the audit-related interfaces and adds the 4 member maps as required. This has cleaned up our profiles a great deal and we'd prefer to keep it this way if possible.

For the purposes of this report however, it is not necessary to perform any action within the callback. Calling the method is enough to trigger the exception.

Removing any one line from the profile will prevent the exception--at the expense of an incorrect profile. Removing the Value property from the Source/Dest classes will also prevent the exception, though this is obviously also incorrect.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions