Search code examples
c#automapper

AutoMapper Pascal case -> lower underscore map doesn't work in reverse


I have a DTO object with a lower-underscore naming convention, which I would like to map to and from a DB entity with a Pascal case naming convention. I'm having issues mapping properties with numbers in the name. Here's a minimum reproducible example:

using AutoMapper;

MapperConfiguration config =
    new(cfg =>
    {
        cfg.SourceMemberNamingConvention = PascalCaseNamingConvention.Instance;
        cfg.DestinationMemberNamingConvention = LowerUnderscoreNamingConvention.Instance;
        cfg.CreateMap<MyDbEntity, MyDto>().ReverseMap();
    });

config.AssertConfigurationIsValid();

IMapper mapper = config.CreateMapper();

// dto.equip_crest_slot_type_1_crest_id_1 = 10
MyDto dto = mapper.Map<MyDto>(new MyDbEntity() { EquipCrestSlotType1CrestId1 = 10 });
// dbEntry.EquipCrestSlotType1CrestId1 = 0
MyDbEntity dbEntry = mapper.Map<MyDbEntity>(dto);

public class MyDto
{
    public int equip_crest_slot_type_1_crest_id_1 { get; set; }
}

public class MyDbEntity
{
    public int EquipCrestSlotType1CrestId1 { get; set; }
}

When creating the DB entity from a DTO, the property is missed out, which is odd since it works mapping one way. I wondered if the problem was that ReverseMap() doesn't validate, and I can see that if I make an actual reverse profile:

using AutoMapper;

MapperConfiguration config =
    new(cfg =>
    {
        cfg.SourceMemberNamingConvention = LowerUnderscoreNamingConvention.Instance;
        cfg.DestinationMemberNamingConvention = PascalCaseNamingConvention.Instance;
        cfg.CreateMap<MyDto, MyDbEntity>();
    });

config.AssertConfigurationIsValid();

that I do get an exception

Unhandled exception. AutoMapper.AutoMapperConfigurationException:
Unmapped members were found. Review the types and members below.
Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type
For no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters
====================
MyDto -> MyDbEntity (Destination member list)
MyDto -> MyDbEntity (Destination member list)

Unmapped properties:
EquipCrestSlotType1CrestId1

I'm not sure why a profile would work one way and not the other, but I assume the issue is that I've misunderstood how Pascal case is supposed to work when numbers are in the middle of a property name. The lower_underscore names are received from a client and I have no control over them, but I can rename the database entities.

Is there a way that I can resolve this without manually assigning the members? I am on AutoMapper 12.0.0 if it matters.


Solution

  • You can always replace the default naming convention if it doesn't work in your particular use case. The issue is that the PascalCaseNamingConvention is tokenizing the name as ["Equip", "Crest", "Slot", "Type1", "Crest" "Id1"], instead of taking the numbers as separate tokens as the LowerUnderscoreNamingConvention is in the lower-underscore names.

    In this case, you can just make a class that is slightly different to the existing PascalCaseNamingConvention, which has the following source code:

    public sealed class PascalCaseNamingConvention : INamingConvention
    {
        public static readonly PascalCaseNamingConvention Instance = new();
        public string SeparatorCharacter => "";
        public string[] Split(string input)
        {
            List<string> result = null;
            int lower = 0;
            for(int index = 1; index < input.Length; index++)
            {
                if (char.IsUpper(input[index]))
                {
                    result ??= new();
                    result.Add(input[lower..index]);
                    lower = index;
                }
            }
            if (result == null)
            {
                return Array.Empty<string>();
            }
            result.Add(input[lower..]);
            return result.ToArray();
        }
    }
    

    Simply change the Split function to tokenize the digits separately.