Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to correctly configure `int?` to `int` projections using AutoMapper?

Tags:

c#

automapper

I'm having some trouble getting this to work correctly. I have two classes:

public class TestClassA
{
    public int? NullableIntProperty { get; set; }
}

public class TestClassB
{
    public int NotNullableIntProperty { get; set; }
}

I then set up the following mappings:

cfg.CreateMap<TestClassA, TestClassB>()
    .ForMember(dest => dest.NotNullableIntProperty,
               opt => opt.MapFrom(src => src.NullableIntProperty));

cfg.CreateMap<TestClassA, TestClassA>()
    .ForMember(dest => dest.NullableIntProperty,
               opt => opt.MapFrom(src => src.NullableIntProperty));

cfg.CreateMap<TestClassB, TestClassA>()
    .ForMember(dest => dest.NullableIntProperty,
               opt => opt.MapFrom(src => src.NotNullableIntProperty));

cfg.CreateMap<TestClassB, TestClassB>()
    .ForMember(dest => dest.NotNullableIntProperty,
               opt => opt.MapFrom(src => src.NotNullableIntProperty));

I now have four mappings set up, and will test the following scenarios:

int? => int
int => int?
int => int
int? => int?

In a test class, I then use the mappings like this:

var testQueryableDest = testQueryableSrc.ProjectTo<...>(_mapper.ConfigurationProvider);

The only projection I would expect not to work at this stage would be TestClassA => TestClassB, since I can see how AutoMapper may not know what to do with the int? in cases where the value is null. Sure enough, that's exactly the case. So I set up a mapping for int? => int like so:

cfg.CreateMap<int?, int>()
    .ProjectUsing(src => src ?? default(int));

This is where things become strange. As soon as I add this mapping, the mapping from TestClassB => TestClassB fails to even create. It gives this error message:

Expression of type 'System.Int32' cannot be used for assignment to type 'System.Nullable`1[System.Int32]'

I find this message incredibly strange as TestClassB does not have an int? on it at all. So what's going on here? I feel like I must be misunderstanding something about how AutoMapper needs these projections to be handled. I realise the various bits of code may be tricky to piece together so here's the entire test class for reference:

[TestClass]
public class BasicTests
{
    private readonly IMapper _mapper;

    public BasicTests()
    {
        var config = new MapperConfiguration(cfg =>
        {
            cfg.CreateMap<int?, int>()
                .ProjectUsing(src => src ?? default(int));

            cfg.CreateMap<TestClassA, TestClassB>()
                .ForMember(dest => dest.IntProperty, opt => opt.MapFrom(src => src.NullableIntProperty));

            cfg.CreateMap<TestClassA, TestClassA>()
                .ForMember(dest => dest.NullableIntProperty, opt => opt.MapFrom(src => src.NullableIntProperty));

            cfg.CreateMap<TestClassB, TestClassA>()
                .ForMember(dest => dest.NullableIntProperty, opt => opt.MapFrom(src => src.IntProperty));

            cfg.CreateMap<TestClassB, TestClassB>()
                .ForMember(dest => dest.IntProperty, opt => opt.MapFrom(src => src.IntProperty));
        });

        _mapper = new Mapper(config);
    }

    [TestMethod]
    public void CanMapNullableIntToInt()
    {
        var testQueryableSource = new List<TestClassA>
        {
            new TestClassA
            {
                NullableIntProperty = null
            }
        }.AsQueryable();

        var testQueryableDestination = testQueryableSource.ProjectTo<TestClassB>(_mapper.ConfigurationProvider);
    }

    [TestMethod]
    public void CanMapNullableIntToNullableInt()
    {
        var testQueryableSource = new List<TestClassA>
        {
            new TestClassA
            {
                NullableIntProperty = null
            }
        }.AsQueryable();

        var testQueryableDestination = testQueryableSource.ProjectTo<TestClassA>(_mapper.ConfigurationProvider);
    }

    [TestMethod]
    public void CanMapIntToNullableInt()
    {
        var testQueryableSource = new List<TestClassB>
        {
            new TestClassB
            {
                IntProperty = 0
            }
        }.AsQueryable();

        var testQueryableDestination = testQueryableSource.ProjectTo<TestClassA>(_mapper.ConfigurationProvider);
    }

    [TestMethod]
    public void CanMapIntToInt()
    {
        var testQueryableSource = new List<TestClassB>
        {
            new TestClassB
            {
                IntProperty = 0
            }
        }.AsQueryable();

        var testQueryableDestination = testQueryableSource.ProjectTo<TestClassB>(_mapper.ConfigurationProvider);
    }
}
like image 464
Nick Coad Avatar asked Oct 12 '16 06:10

Nick Coad


People also ask

Where is AutoMapper configuration?

Where do I configure AutoMapper? ¶ Configuration should only happen once per AppDomain. That means the best place to put the configuration code is in application startup, such as the Global.


1 Answers

I’ve found the shortest way to reproduce this situation is the following:

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<int?, int>().ProjectUsing(x => x ?? default(int));
    cfg.CreateMap<TestClassA, TestClassA>()
        .ForMember(a => a.NullableIntPropety, o => o.MapFrom(a => a.NullableIntProperty));
}

It seems to me that AutoMapper is attempting to use the int? => int mapper here although a more obvious identity-based mapping would be to use here.

Since every int is also a valid int?, AutoMapper attempts to use the int? => int mapper here and assign the result to the int? member. But it seems that under the hood something does not correctly work when resolving just that assignment, hence that exception.

What seems to fix it is to add another mapping, an identity mapping for int? => int?:

cfg.CreateMap<int?, int?>().ProjectUsing(x => x);

Then, this mapping is being used instead and no exception occurs (and the mapping also properly works—with all of your examples).


This problem seems to exist on the current AutoMapper 5.1.x release (current is 5.1.1). The good news is, that it has already been fixed. If you try the current 5.2 alpha from the myget feed, then the code works fine without any issues.

Since the 5.1.1 release, the code base has seen quite a few contributions with multiple fixes on nullable mapping (e.g. this and this pull request). I assume that one of those changes has fixed this problem.

Most likely, it was pull request #1672 which just meant to remove unneeded code but apparently also fixed issue 1664 which was about AutoMapper apparently prioritizing nullable source mappings over non-nullable sources even if a non-nullable source was being mapped. And that sounds very much like this very problem you have experienced.

So, for now, you can add above workaround to map the type to itself or use an alpha release, while we wait for 5.2 to be released.

like image 125
poke Avatar answered Oct 12 '22 23:10

poke