AutoMapper: Problem with mapping records type

Viewed 6080

I am mapping with automapper 10.1.1 in c# 9 from this class

public record BFrom 
{
    public Guid Id { get; init; }
    public Guid DbExtraId { get; init; }
}

into this

public record ATo(Guid Id, Guid ExtraId) : BaseTo(Id);

And have the following configuration

CreateMap<BFrom, ATo>()
    .ForMember("ExtraId", opt => opt.MapFrom(src => src.DbExtraId))
    .ReverseMap();

But I have a problem when I am trying to map it throws an exception with a message that needs to have a constructor with 0 args or only optional args. Is it possible to fix this problem without changing the record types?

4 Answers

I had the same issue, and ended up creating this extension method to solve it:

public static class AutoMapperExtensions
{
    public static IMappingExpression<TSource, TDestination> MapRecordMember<TSource, TDestination, TMember>(
        this IMappingExpression<TSource, TDestination> mappingExpression,
        Expression<Func<TDestination, TMember>> destinationMember, Expression<Func<TSource, TMember>> sourceMember)
    {
        var memberInfo = ReflectionHelper.FindProperty(destinationMember);
        string memberName = memberInfo.Name;
        return mappingExpression
            .ForMember(destinationMember, opt => opt.MapFrom(sourceMember))
            .ForCtorParam(memberName, opt => opt.MapFrom(sourceMember));
    }
}

Then you simply use it like this:

CreateMap<BFrom, ATo>()
    .MapRecordMember(a => a.ExtraId, src => src.DbExtraId)
    .ReverseMap();

and it will take care of registering both constructor and member so that you don't run into issues like these.

What you need, is to specify the constructor-parameter by name in your mapping profile like so public AToProfile() => CreateMap<BFrom, ATo>().ForCtorParam(ctorParamName: "ExtraId", m => m.MapFrom(s => s.DbExtraId)).ReverseMap(); That way, AutoMapper will know, from where the value should be taken and will map accordingly.

public class StackoverflowQuestionTest
{
    [Fact]
    public void BFrom_MapTo_ATo()
    {
        // Arrange
        IConfigurationProvider configuration = new MapperConfiguration(cfg => cfg.AddProfile<AToProfile>());
        var source = new BFrom {Id = Guid.NewGuid()};

        // Act
        var target = new Mapper(configuration).Map<ATo>(source);

        // Assert
        target.Id.Should().Be(source.Id);
    }


}

public record BaseTo (Guid Id); // this is an assumption, as you did not specify BaseTo in you question

public record BFrom
{
    public Guid Id { get; init; }
    public Guid DbExtraId { get; init; }
}

public record ATo(Guid Id, Guid ExtraId) : BaseTo(Id);

public class AToProfile : Profile
{
    public AToProfile() =>
        CreateMap<BFrom, ATo>()
            .ForCtorParam(ctorParamName: "ExtraId", m => m.MapFrom(s => s.DbExtraId))
            .ReverseMap();
}

Try

CreateMap<BFrom, ATo>().DisableCtorValidation()
    .ForMember("ExtraId", opt => opt.MapFrom(src => src.DbExtraId))
    .ReverseMap()

Use a more static approach.. we know the record constructor name..

public record ATo(Guid Id, Guid ExtraId) : BaseTo(Id);

CreateMap<BFrom, ATo>()
    .ForMember(nameof(ATo.Extra), opt => opt.MapFrom(src => src.DbExtraId))
    .ReverseMap();

Explanation / Deepish dive

Records are really just syntax sugar for a concept that's been around forever, that is they offer immutable properties. Records actually make AutoMapper with the old concept easier, and safer because with the new syntax we know what the property names will be, i.e record Example(string SomeProperty). The compiler will just create a constructor like so,

public class Example
{
    public string SomeProperty { get; init; }         
    Example(string SomeProperty) 
    {
        this.SomeProperty = SomeProperty;
    }
}

Making an empty constructor wont help you because the properties are init only, and you cant really use object initializer because you have to use the constructor... and well object initializers are a special case for init properties, that is they allow the properties to change even though object initializers are really just syntax sugar themselves. There is not really a way via reflection to use object initializers, so that loophole is gone for libraries like AutoMapper, the loophole being.. new Example(default){SomeProperty = "SomeValue"}.

Now of course you can use reflection to change readonly/private/internal properties, which I am not sure if AutoMapper supports or not, I hope it doesn't because its not a very good thing to do for reasons well documented on the internet.

Related