Dennis
Dennis

Reputation: 14495

AutoMapper - Different ProjectUsings for nullable and non-nullable ints

I need to project integer values from the database to enum values using Entity Framework and AutoMapper. The problems seems to be that the columns can be nullable in some cases and non-nullable in other cases. If they are of a nullable type, I want to use a default value (first enum value) for nulls.

Here is a complete, minimal example I put together with my current approach. It is a console application with current nuget packages of Entity Framework and AutoMapper installed. It also needs a database (for database first) with a table like the following:

CREATE TABLE [dbo].[MyTable] (
    [Id] INT PRIMARY KEY,
    [EnumValue] INT NOT NULL,
    [EnumValueNullable] INT
)

The code for the console application (C#):

public enum MyEnum
{
    Value1 = 0,
    Value2 = 1
}

public class MyTableModel
{
    public int Id { get; set; }
    public MyEnum EnumValue { get; set; }
    public MyEnum EnumValueNullable { get; set; }
}

public class MyProfile : Profile
{
    public MyProfile()
    {
        this.CreateMap<MyTable, MyTableModel>();
        this.CreateMap<int, MyEnum>().ProjectUsing(x => (MyEnum)x);
        this.CreateMap<int?, MyEnum>().ProjectUsing(x => x.HasValue ? (MyEnum)x.Value : MyEnum.Value1);
    }
}

static void Main(string[] args)
{
    var config = new MapperConfiguration(x => x.AddProfile(new MyProfile()));
    var result = new MyDataEntities().MyTable.ProjectTo<MyTableModel>(config).ToList();
}

When executing this code, it tells me that HasValue is not defined for type System.Int32. This is of course correct, but I assumed AutoMapper would pick the version specified for non-nullable integers. Removing either maps (for int and int?) does not help, as well as removing them both or changing the order.

As a side note, I am migrating AutoMapper from version 3.3.1.0 in a larger project. It seems to have worked with both maps defined in that version.

Upvotes: 4

Views: 891

Answers (2)

Ivan Stoev
Ivan Stoev

Reputation: 205849

There is (IMHO) a bug in the current AutoMapper MapperConfiguration class which is causing the specified mapping for Nullable<TSource> -> TDestination to be used (overriding the already specified) for TSource -> TDestination. It can easily be seen by executing the following code:

var config = new MapperConfiguration(x => x.AddProfile(new MyProfile()));
var m1 = config.ResolveTypeMap(typeof(int), typeof(MyEnum));
var m2 = config.ResolveTypeMap(typeof(int?), typeof(MyEnum));
Console.WriteLine(m1 == m2); // true

The resulting effect is the aforementioned runtime exception when AutoMapper is trying to map TSource to TDestination (in your case, int to MyEnum for EnumValue property) using the specified expression (because x is int, not int? as expected, hence has no HasValue and Value properties).

As a workaround (or general solution?) I would suggest defining only the map from int to enum, and using the AutoMapper Null substitution feature for the other part. Since currently null substitution is supported only at property map level, to make it easier I would encapsulate it in a helper method like this:

public static class AutoMapperExtensions
{
    public static void NullSubstitute<TSource, TDestination>(this Profile profile, TSource nullSubstitute)
        where TSource : struct
    {
        object value = nullSubstitute;
        profile.ForAllPropertyMaps(
            map => map.SourceType == typeof(TSource?) && 
                   map.DestinationPropertyType == typeof(TDestination),
            (map, config) => config.NullSubstitute(value)
        );
    }
}

and use it like this:

public class MyProfile : Profile
{
    public MyProfile()
    {
        this.CreateMap<MyTable, MyTableModel>();
        this.CreateMap<int, MyEnum>().ProjectUsing(x => (MyEnum)x);
        this.NullSubstitute<int, MyEnum>((int)MyEnum.Value1);
    }
}

Upvotes: 4

juunas
juunas

Reputation: 58898

Alternatively you could try to do the mapping like this:

this.CreateMap<MyTable, MyTableModel>()
     .ForMember(dest => dest.EnumValue, o => o.MapFrom(src => (MyEnum)src.EnumValue))
     .ForMember(dest => dest.EnumValueNullable, o => o.MapFrom(src => src.EnumValueNullable != null ? src.EnumValueNullable.Value : MyEnum.Value1));

Upvotes: 1

Related Questions