AutoMapper - Mapping collection of the same types not using mapping profile

Viewed 145

I have a problem with mapping collections of the same type objects with AutoMapper. Let me give you an example :

First, object classes:

public class ClassA
{
     public string Name { get; set; }
     public string Type { get; set; }
}

public class ClassB
{
     public string Name { get; set; }
     public List<ClassA> Classes { get; set; }
}

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<ClassA, ClassA>()
            .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
            .ForAllOtherMembers(opts => opts.Ignore());

        CreateMap<ClassB, ClassB>()
            .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
            .ForMember(dest => dest.Classes, opts => opts.MapFrom(src => src.Classes))
            .ForAllOtherMembers(opts => opts.Ignore());
    }
}

And now when executing such code:

 List<ClassB> targetList;

 targetList = DbContext.ClassesB.ProjectTo<ClassB>(Mapper.ConfigurationProvider).ToList();

Mapping does not work correctly. ClassB.Name is mapped correctly, but it looks like that mapping definition for ClassA is ignored, because all properties are mapped. Additionally, when I change ClassB.Classes property to no-list (ClassA) mapping work correctly.

Is it AutoMapper's error that it is ignoring defined mappings?

1 Answers

If you look at the expression that is generated from ProjectTo<ClassB>, you can see how the mapping actually goes.

var expression = DbContext.ClassesB.ProjectTo<ClassB>(Mapper.ConfigurationProvider).Expression;
Console.WriteLine(expression);

results to:

[Microsoft.EntityFrameworkCore.Query.QueryRootExpression]
    .Select(dtoClassB => new ClassB() {Classes = dtoClassB.Classes, Name = dtoClassB.Name})

Here, List<ClassA> is not mapped item by item and is simply assigned to the destination object. I don't know if this is done purposely by design or is it a bug.

With static API you can workaround the problem with the following configuration:

CreateMap<ClassA, ClassA>()
    .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
    .ForAllOtherMembers(opts => opts.Ignore());

CreateMap<ClassB, ClassB>()
    .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
    .ForMember(dest => dest.Classes, opts => opts.MapFrom(src => src.Classes.Select(a => Mapper.Map<ClassA>(a)).ToList()))
    .ForAllOtherMembers(opts => opts.Ignore());

The workaround for Instance API is not very practical and cumbersome, but I still bring it here. You have to create 2 profiles, one of which is only for mapping ClassA

public class MappingClassAProfile : Profile
{
    public MappingClassAProfile()
    {
        CreateMap<ClassA, ClassA>()
            .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
            .ForAllOtherMembers(opts => opts.Ignore());
    }
}

public class MappingProfile : MappingClassAProfile
{
    public MappingProfile(IMapper mapper) : base()
    {
        CreateMap<ClassB, ClassB>()
            .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
            .ForMember(dest => dest.Classes, opts => opts.MapFrom(src => src.Classes.Select(a => mapper.Map<ClassA>(a)).ToList()))
            .ForAllOtherMembers(opts => opts.Ignore());
    }
}

Then you construct mappers as follows:

var configurationBase = new MapperConfiguration(cfg =>
{
    cfg.AddProfile<MappingClassAProfile>();
});

var mapperA = configurationBase.CreateMapper();

var configuration = new MapperConfiguration(cfg =>
{
    cfg.AddProfile(new MappingProfile(mapperA));
});

var mapper = configuration.CreateMapper(); // use it to project ClassB instances
Related