1

IncludeMembers() will always map from first match even if object is null.

Let's say we have the following source models:

public class Item
{
    public MovieMetadata MovieMetadata { get; set; }

    public BookMetadata BookMetadata { get; set; }
}

public class MovieMetadata
{
    public string Title { get; set; }
}

public class BookMetadata
{
    public string Title { get; set; }
}

Destination model:

public class ItemDetail
{
    public string Title { get; set; }
}

Mapping profile:

public class ItemProfile : Profile
{
    public ItemProfile()
    {
        CreateMap<Item, ItemDetail>()
            .IncludeMembers(
            src => src.BookMetadata,
            src => src.MovieMetadata);

        CreateMap<BookMetadata, ItemDetail>();

        CreateMap<MovieMetadata, ItemDetail>();
    }
}

And Program class with instantiation and testing logic:

public class Program
{
    public static void Main()
    {
        //create mapper
        var configuration = new MapperConfiguration(cfg =>
        {
            cfg.AddMaps(typeof(ItemProfile));
        });
        var mapper = configuration.CreateMapper();
        
        //check if configuration valid
        mapper.ConfigurationProvider.AssertConfigurationIsValid();
        Console.WriteLine("Mapper configuration is valid");
        
        //try map book metadata
        var bookItem = new Item
        {
            BookMetadata = new BookMetadata()
            {
                Title = "book"
            },
            MovieMetadata = null
        };
        var bookItemDetail = mapper.Map<ItemDetail>(bookItem);
        bool isBookCorrectlyMapped = bookItem.BookMetadata.Title == bookItemDetail.Title;
        Console.WriteLine($"Book mapped correctly: {isBookCorrectlyMapped}");
        
        //try map movie metadata
        var movieItem = new Item
        {
            BookMetadata = null,
            MovieMetadata = new MovieMetadata()
            {
                Title = "movie"
            }
        };
        var movieItemDetail = mapper.Map<ItemDetail>(movieItem);
        bool isMovieCorrectlyMapped = movieItem.MovieMetadata.Title == movieItemDetail.Title;
        Console.WriteLine($"Movie mapped correctly: {isMovieCorrectlyMapped}");
    }
}

This is the output we will see:

Mapper configuration is valid
Book mapped correctly: True
Movie mapped correctly: False

We see that mapping for item with BookMetadata succeeded, but for MovieMetadata failed. Need to make updates so this test succeeds.

Suppose the reason it's failing is because of order of items in IncludeMembers():

.IncludeMembers(
            src => src.BookMetadata,
            src => src.MovieMetadata)

Here we have src.BookMetadata first and src.MovieMetadata second. During mapping it will find Title field in src.BookMetadata and will always use this value even if src.BookMetadata is null.

Is there any way to skip src.BookMetadata if it is null and use next src.MovieMetadata instead? Or probably need to use something else instead of IncludeMembers()?

Here is similar issue: https://github.com/AutoMapper/AutoMapper/issues/3204

Code above you can find here to quickly reproduce issue: https://dotnetfiddle.net/9YLGR6

1 Answers1

1

Found possible solutions.

Option 1. IValueResolver

We can create custom value resolver. This solution is good for small amount of fields that we need to map from child objects. As any field will require specific value resolver.

Let's create TitleResolver:

public class TitleResolver : IValueResolver<Item, ItemDetail, string>
{
    public string Resolve(Item source, ItemDetail destination, string destMember, ResolutionContext context)
    {
        if (source != null)
        {
            if (source.BookMetadata != null)
            {
                return source.BookMetadata.Title;
            }
            if (source.MovieMetadata != null)
            {
                return source.MovieMetadata.Title;
            }
        }
        return null;
    }
}

And update ItemProfile:

public class ItemProfile : Profile
{
    public ItemProfile()
    {
        CreateMap<Item, ItemDetail>()
            .ForMember(dest => dest.Title, opt => opt.MapFrom<TitleResolver>());
    }
}

Link to whole code sample: https://dotnetfiddle.net/6pfKYh

Option 2. BeforeMap()/AfterMap()

In case if our child objects have more than one field to be mapped to destination object it may be a good idea to use BeforeMap() or AfterMap() methods. In that case ItemProfile will be updated to:

public class ItemProfile : Profile
{
    public ItemProfile()
    {
        CreateMap<Item, ItemDetail>(MemberList.None)
            .AfterMap((src, dest, ctx) => 
            {
                if (src.BookMetadata != null)
                {
                    ctx.Mapper.Map(src.BookMetadata, dest);
                }
                else if (src.MovieMetadata != null)
                {
                    ctx.Mapper.Map(src.MovieMetadata, dest);
                }
            });

        CreateMap<BookMetadata, ItemDetail>();

        CreateMap<MovieMetadata, ItemDetail>();
    }
}

Link to whole code sample: https://dotnetfiddle.net/ny1yRU