AutoMapper mapping between enum and its integer values fails with ReverseMap

2k views Asked by At

The application is built with DDD approach, with a separate set of persistence models. I called database object, or dbo:

public class ParentDbo
{
    public int ParentId { get; set; }
    public int TypeId { get; set; }
}

public class ChildDbo
{
    public int ChildId { get; set; }
    public ParentDbo Parent { get; set; }
    public int RetryNumber { get; set; }
}

We have a simple model to look at: a parent and a child relationship. The RetryNumber presents the enum value in the database.


On retrieving data, it uses Dapper to first query the database, and use its splitOn feature to map data into them. This part is irrelevant but I will show it anyway for completeness:

const string sql = "SELECT * FROM XXX ....";

using (var cnt = _dbConnectionFactory.CreateConnection())
{
    var childDbos = await cnt.QueryAsync<ChildDbo, ParentDbo, ChildDbo>(
        sql: sql,
        map: (childDbo, parentDbo) =>
        {
            childDbo.Parent = parentDbo;
            return childDbo;
        },
        splitOn: "ParentId"
    );
}

Dapper has limitation that it couldn't map data to private complex objects. That's mainly the reason why I have to have 2 sets of models. I would like to encapsulate the data and logic within domain models, with private setters and other techniques.

Here are my domain models:

public class Parent
{
    public int Id { get; private set; }
    public int TypeId { get; private set; }
    
    public Parent(int parentId, int typeId)
    {
        // Validations
        
        this.Id = parentId;
        this.TypeId = typeId;
    }
}

public class Child
{
    public int Id { get; private set; }
    public Parent Parent { get; private set; }
    public Attempt Attempt { get; private set; }
    
    public Child(int childId, Parent parent, Attempt attempt)
    {
        // Validations
        
        this.Id = childId;
        this.Parent = parent;
        this.Attempt = attempt;
    }
}

For domain models, I don't want public setters, and parameter-less constructors.

The Attempt is the enum with integer backing values:

public enum Attempt
{
    Original = 1,
    FirstRetry = 2,
    SecondRetry = 3,
    LastRetry = 4
}

Lastly, I want to use AutoMapper to map between Dbos and the domain models. Here is the mapping:

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<Child, ChildDbo>()
            .ForMember(dest => dest.ChildId, opts => opts.MapFrom(src => src.Id))
            .ForMember(dest => dest.RetryNumber, opts => opts.MapFrom(src => (int)src.Attempt))
            .ReverseMap();
        
        CreateMap<Parent, ParentDbo>()
            .ForMember(dest => dest.ParentId, opts => opts.MapFrom(src => src.Id))
            .ReverseMap();
    }
}

I want to have two-ways mappings so I use ReverseMap().


.Net Fiddle demo: https://dotnetfiddle.net/saEHWd

It maps domain models to dbos without problem:

enter image description here

But its reverse, mapping from dbos to domain models, is throwing exceptions:

Unhandled exception. System.ArgumentException: Program+Child needs to have a constructor with 0 args or only optional args. (Parameter 'type') at lambda_method18(Closure , Object , Child , ResolutionContext ) at AutoMapper.Mapper.MapCore[TSource,TDestination](TSource source, TDestination destination, ResolutionContext context, Type sourceType, Type destinationType, IMemberMap memberMap) at AutoMapper.Mapper.Map[TSource,TDestination](TSource source, TDestination destination) at AutoMapper.Mapper.Map[TDestination](Object source) at Program.Main()


I've tried to remove the enum property and everything worked so I'm pretty sure it's the enum mapping that's having issues.

1

There are 1 answers

1
Guru Stron On BEST ANSWER

As far as I can see in your fiddle you are trying to map from ChildDbo to Parent and there is no mapping setup for it. Change the mapping code to:

var child2 = mapper.Map<Child>(childDbo);

And since there is mismatch in third Child's ctor param and source property names change map to:

CreateMap<Child, ChildDbo>()
    .ForMember(dest => dest.ChildId, opts => opts.MapFrom(src => src.Id))
    .ForMember(dest => dest.RetryNumber, opts => opts.MapFrom(src => (int)src.Attempt))
    .ReverseMap()
    .ConstructUsing((dbo, ctx) => new Child(dbo.ChildId, ctx.Mapper.Map<Parent>(dbo.Parent), (Attempt)dbo.RetryNumber));

See here

Or rename third Child's ctor parameter to retryNumber:

public Child(int childId, Parent parent, Attempt retryNumber)

see here.

or use ForCtorParam:

CreateMap<Child, ChildDbo>()
    .ForMember(dest => dest.ChildId, opts => opts.MapFrom(src => src.Id))
    .ForMember(dest => dest.RetryNumber, opts => opts.MapFrom(src => (int)src.Attempt))
    .ReverseMap()
    .ForCtorParam("attempt", opt => opt.MapFrom(dbo => dbo.RetryNumber))

Here.