martinoss
martinoss

Reputation: 5458

Automapper: Unexpected mapping behavior using nullable type

With Automapper (6.6.2) I'm trying to map a nullable boolean to a destinations object property (dest.IsEnabled.Value). But as soon the source value is null, the entire destination property (dest.IsEnabled) is null. But I only expect the property "value" to be null.

Any idea how to do it right?

class Program
{
    static void Main(string[] args)
    {
        var source = new Source();
        source.IsEnabled = null;

        var config = new MapperConfiguration(cfg =>
        {
            cfg.CreateMap<Source, Dest>();
                //.ConstructUsing(ctor => new Dest());
            cfg.CreateMap<bool?, IsEnabledProperty>()
                .ForMember(dst => dst.Value, opt => opt.MapFrom(src => src));
        });

        //var debug = config.BuildExecutionPlan(typeof(Source), typeof(Dest));

        var mapper = config.CreateMapper();

        //var dest = mapper.Map<Source, Dest>(source, new Dest());
        var dest = mapper.Map<Source, Dest>(source);

        if (dest.IsEnabled == null)
        {
            Console.WriteLine("IsEnabled is null. But why? I expect IsEnabled.Value to be null.");
        }

        Console.ReadLine();
    }
}

class Source
{
    public bool? IsEnabled { get; set; }
}

class Dest
{
    public IsEnabledProperty IsEnabled { get; set; } 
        = new IsEnabledProperty() { IsRequired = true };

    // Just to check if the property is initialized
    public OtherProperty OtherProperty { get; set; }
        = new OtherProperty() { IsRequired = true };
}

class IsEnabledProperty
{
    public bool IsRequired { get; set; }
    public bool? Value { get; set; }
}

class OtherProperty
{
    public bool IsRequired { get; set; }
    public bool? Value { get; set; }
}

Update

When I update the mapping like this

cfg.CreateMap<bool?, IsEnabledProperty>()
   .ConvertUsing((src, dst, context) => 
   {
      if (dst != null) 
         dst.Value = src;
      return dst;
   });

dest.IsEnabled.Value is mapped correctly for all variants (null, false, true), and also the property destination.IsEnabled.IsRequired has the initialized value of true, but it forces me to pass an instance of destination to the mapping method: var dest = mapper.Map<Source, Dest>(source, new Dest());. This doesn't make sense in my eyes, since I expect the destination object should be constructed the same way by automapper?

When looking at the execution plan, I'm asking me, why it checks for (dest == null) in line 11 and not just use the initialized typeMapDestination:

(src, dest, ctxt) =>
{
    Dest typeMapDestination;
    return (src == null)
        ? null
        : {
            typeMapDestination = dest ?? new Dest();
            try
            {
                var resolvedValue = ((src == null) || false) ? null : src.IsEnabled;
                var propertyValue = mappingFunction.Invoke(resolvedValue, (dest == null) ? null : typeMapDestination.IsEnabled, ctxt);
                typeMapDestination.IsEnabled = propertyValue;
            }
            catch (Exception ex)
            {
                throw new AutoMapperMappingException(
                    "Error mapping types.",
                    ex,
                    AutoMapper.TypePair,
                    TypeMap,
                    PropertyMap);
            };

            return typeMapDestination;
        };
}

Upvotes: 1

Views: 918

Answers (1)

Fabian Claasen
Fabian Claasen

Reputation: 284

I think you want to map the whole bool? instead of only the value dest.IsEnabled.Value. This way you can ask the boolean if it has a value with dest.HasValue and use the value with dest.Value.

You might want to add AllowNullableMapping to the Automapper configuration.

Upvotes: 1

Related Questions