'ProjectTo behaves differently than expected when mapping a member via an interface

I discovered some unexpected behavior in my mappings that might be an AutoMapper bug, but there is enough complexity that I might be missing some nuance, somewhere. See this gist for full (minimal) code to demonstrate. (I tested with AutoMapper 11.0.1)

I have some ViewModels that all share a member that requires some special mapping from a Model, so I have set up an interface, so that I can define that mapping once, and configure it for all those VMs:

class Model {
    public int First { get; set; }
    public int Second { get; set; }
}
interface IIsSpecialVm {
    public bool Special { get; set; }
}
class ProductVm : IIsSpecialVm {
    public int Product { get; set; }
    public bool Special { get; set; }
}
class SumVm : IIsSpecialVm {
    public int Sum { get; set; }
    public bool Special { get; set; }
}
...
    var configuration = new MapperConfiguration(config =>
    {
        config.CreateMap<Model, IIsSpecialVm>()
            .ForMember(x => x.Special, y => y.MapFrom(src => src.First < 10 && src.Second > 10));

        config.CreateMap<Model, ProductVm>()
            .IncludeBase<Model, IIsSpecialVm>()
            .ForMember(x => x.Product, opt => opt.MapFrom(src => src.First * src.Second));

        config.CreateMap<Model, SumVm>()
            .IncludeBase<Model, IIsSpecialVm>()
            .ForMember(x => x.Sum, opt => opt.MapFrom(src => src.First + src.Second));

        ...

    });
    IMapper mapper = configuration.CreateMapper();

Thus far, it all works great. I can map these VMs individually, or use .ProjectTo<>() from an IQueryable and I get the results I expect.

The problem comes when I introduce a container model that has a Model member, and a VM that needs to map from it:

class ContainingModel {
    public Model Model { get; set; }
    public string Operator { get; set; }
}
class ExpressionVm : IIsSpecialVm {
    public string Expression { get; set; }
    public bool Special { get; set; }
}

I need to add a mapping for these that includes an AfterMap() to map the Model onto the ExpressionVm, so that I can pick up my special member, and, of course, I need to create a mapping from the Model to ExpressionVm to make sure that it uses the interface:

    var configuration = new MapperConfiguration(config =>
    {
        ... // See above

        config.CreateMap<Model, ExpressionVm>().IncludeBase<Model, IIsSpecialVm>();

        config.CreateMap<ContainingModel, ExpressionVm>()
            .ForMember(x => x.Expression, opt => opt.MapFrom(src => $"{src.Model.First} {src.Operator} {src.Model.Second}"))
            .AfterMap((s, d, ctx) => { ctx.Mapper.Map(s.Model, d); });

    });
    IMapper mapper = configuration.CreateMapper();

Mapping Models to ExpressionVms, is not something I want to do on its own, but it does work, whether I Map or ProjectTo:

    var models = new List<Model> {
        new Model { First = 1, Second = 8 },
        new Model { First = 2, Second = 18 },
    };

    IEnumerable<ExpressionVm> mappedList = models.Select(m => mapper.Map<ExpressionVm>(m));
    //IQueryable<ExpressionVm> mappedList = models.AsQueryable().ProjectTo<ExpressionVm>(mapper.ConfigurationProvider);
    foreach (var x in mappedList)
    {
        Console.WriteLine(JsonSerializer.Serialize(x));
    }

    // Output in either case:
    // {"Expression":null,"Special":false}
    // {"Expression":null,"Special":true}

However, mapping from the ContainingModel to the ExpressionVm surprised me on the ProjectTo:

    var containers = models.Select(m => new ContainingModel { Operator = "%", Model = m });

    IEnumerable<ExpressionVm> mappedList = containers.Select(m => mapper.Map<ExpressionVm>(m));
    foreach (var x in mappedList)
    {
        Console.WriteLine(JsonSerializer.Serialize(x));
    }
    // Output:
    // {"Expression":"1 % 8","Special":false}
    // {"Expression":"2 % 18","Special":true}

    IQueryable<ExpressionVm> projectedList = containers.AsQueryable().ProjectTo<ExpressionVm>(mapper.ConfigurationProvider);
    foreach (var x in projectedList)
    {
        Console.WriteLine(JsonSerializer.Serialize(x));
    }
    // Output:
    // {"Expression":"1 % 8","Special":false}
    // {"Expression":"2 % 18","Special":false}

The interface member, Special, doesn't map across, but remains false.

Thanks for reading this far, and sorry I couldn't make it any more brief.

What's the verdict? Am I missing something, or is this a bug?



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source