Search code examples
c#unit-testingautomapperxunit

AutoMapper with ConstructUsing to map child properties not valid in test, but works in production


I am trying to test the usage of our AutoMapper profiles in our project. I have an object Source which I map to Destination, actually it has about 30 properties, but for this example it will be fine with just two. Now I got another object SourceWithChild, which I would like to also map to same destination object. It actually has all the Source data in a child property, but also a little additional data, in this example just one field. To map these I use ConstructUsing, a solution from another stack overflow post. This works great in the application, but in the test the configuration is not valid for AutoMapper.

Here is an example that reproduces this. The second test AutoMapper_ConstructUsing_Test_IsValid will fail, while the other two will complete successfully.

internal class Source
{
    public int IntValue { get; set; }
    public string? StringValue { get; set; }
}

internal class SourceWithChild
{
    public Source Source { get; set; }
    public string AdditionalField { get; set; }
}

internal class Destination
{
    public int IntValue { get; set; }
    public string? StringValue { get; set; }
    public string AdditionalField { get; set; }
}

internal class ChildMappingProfile : Profile
{
    public ChildMappingProfile()
    {
        CreateMap<Source, Destination>()
            .ForMember(dest => dest.IntValue, opt => opt.MapFrom(src => src.IntValue)) // not needed, same name
            .ForMember(dest => dest.StringValue, opt => opt.MapFrom(src => src.StringValue)) // not needed, same name
            .ForMember(dest => dest.AdditionalField, opt => opt.Ignore())
            ;
    }
}

internal class ConstructUsingMappingProfile : Profile
{
    public ConstructUsingMappingProfile()
    {
        CreateMap<SourceWithChild, Destination>()
            .ForMember(dest => dest.AdditionalField, opt => opt.MapFrom(src => src.AdditionalField)) // not needed, same name
            .ConstructUsing((src, ctx) => ctx.Mapper.Map<Destination>(src.Source))
            ;
    }
}

public class ConstructUsingTests
{
    [Fact]
    public void AutoMapper_ChildMapping_IsValid()
    {
        var config = new MapperConfiguration(cfg => cfg.AddProfile(new ChildMappingProfile()));
        config.AssertConfigurationIsValid();
    }

    [Fact]
    public void AutoMapper_ConstructUsing_Test_IsValid()
    {
        var config = new MapperConfiguration(cfg =>
        {
            cfg.AddProfile(new ChildMappingProfile());
            cfg.AddProfile(new ConstructUsingMappingProfile());
        });
        config.AssertConfigurationIsValid();
    }

    [Fact]
    public void AutoMapper_ConstructUsing_Test_Maps_correctly()
    {
        var config = new MapperConfiguration(cfg =>
        {
            cfg.AddProfile(new ChildMappingProfile());
            cfg.AddProfile(new ConstructUsingMappingProfile());
        });
        var destination = config.CreateMapper().Map<Destination>(new SourceWithChild { AdditionalField = "2", Source = new Source { IntValue = 1, StringValue = "Test" } });
        destination.Should().NotBeNull();
        destination.AdditionalField.Should().Be("2");
        destination.IntValue.Should().Be(1);
        destination.StringValue.Should().Be("Test");
    }
}

Now I could just map all the child properties manually in ConstructUsingMappingProfile, but in the real example this will be about 30 properties, and it would also increase additional maintenace cost, since any changes would need to happen in both profiles. I would like to avoid this.

Is there a another way to do this, which will work in both, in tests and in the final application?

Or might this be a bug? I am assuming the test uses a different mapping context and this might even be by design. I also found a GitHub issue in the AutoMapper repo, but they only suggested to ask here.


Solution

  • AutoMapper supports adding child objects to the source with IncludeMembers() method.

    XML Doc documentation of the method:

    Add extra configuration to the current map by also mapping the specified child objects to the destination object. The maps from the child types to the destination need to be created explicitly.

    Add IncludeMembers() call while specifying path to the child object:

    CreateMap<SourceWithChild, Destination>()
        .IncludeMembers(src => src.Source);
    

    Complete mapping profiles:

    internal class ChildMappingProfile : Profile
    {
        public ChildMappingProfile()
        {
            CreateMap<Source, Destination>()
                .ForMember(dest => dest.AdditionalField, opt => opt.Ignore());
        }
    }
    
    internal class ConstructUsingMappingProfile : Profile
    {
        public ConstructUsingMappingProfile()
        {
            CreateMap<SourceWithChild, Destination>()
                .IncludeMembers(src => src.Source);
        }
    }
    

    Do notice how I removed redundant configuration parts, while still keeping the configuration valid.