'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 Model
s to ExpressionVm
s, 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 |
---|