Skip to content

MapAtRuntime with with non-nullable to nullable mapping fails when extension methods are used #4597

@pfeigl

Description

@pfeigl

We ran into a very corner cased problem which is as follows:

  • When MapAtRuntime is used
  • When mapping for example from int to int?
  • When using a extension method in the source mapping

The following Exception shows up:

System.ArgumentException: Expression of type 'System.Nullable`1[System.Int32]' cannot be used for parameter of type 'System.Int32' of method 'System.Nullable`1[System.Int32] MapInternal[Int32,Nullable`1](Int32, System.Nullable`1[System.Int32], AutoMapper.MemberMap)' (Parameter 'arg0')
   at System.Dynamic.Utils.ExpressionUtils.ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arguments, ParameterInfo pi, String methodParamName, String argumentParamName, Int32 index)
   at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, Expression arg0, Expression arg1, Expression arg2)
   at AutoMapper.Execution.ExpressionBuilder.ContextMap(TypePair typePair, Expression sourceParameter, Expression destinationParameter, MemberMap memberMap)
   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.<Main>$(String[] args) in Program.cs:line 12

Here is a minimal reproduceable for the problem:

using AutoMapper;

var cfg = new MapperConfigurationExpression();

cfg.CreateMap<SourceObject, DestObject>()
    .ForMember(dest => dest.MyValue, opt =>
    {
        opt.MapFrom(src => StringExtensions.ExtractInt(src.MyValue));
        opt.MapAtRuntime();
    });

var mapper = new MapperConfiguration(cfg).CreateMapper();

var s = new SourceObject { MyValue = "1" };
var d = mapper.Map<SourceObject, DestObject>(s);

Console.WriteLine(d.MyValue);

class SourceObject { public string MyValue { get; set; } }
class DestObject { public int? MyValue { get; set; } }

static class StringExtensions
{
    public static int ExtractInt(this string str) => int.Parse(str);
}

Removing the this in the extension methods makes everything work.

For what it's worth, the "difference" in the code is here:

if (chain.Count < min || chain.Peek().Target is not ParameterExpression parameter)

For the extension method, the chain is actual equal to min, while with non extension methods, the chain count is 0.

I think, this here was the same problem:
#3819

Thanks,
Philipp

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions