I have an object with collections of objects etc etc 5-6 levels deep. Automapper works well, but there is a level in the middle of the hierarchy where there is a slight structural discrepancy between the source/destination types and I wonder if this means that I can't use AutoMapper.
Given the source object myData
myData.As = {p1,p2,p3}
myData.Bs = {p4,p5,p6}
I would like Automapper to Map the data as
myDstData.Items = {{'A',x1},{'A',x2},{'A',x3},{'B',x4},{'B',x5},{'B',x6}}
All pN are of same type in the source data and all xN the same type in the destination data. Appropriate mapping configuration rule p->x already exist. Automapper should proceed to handle those.
It would also be interesting if it could be done both ways. That a collection can be split.
Whenever such hard question about AutoMapper arises, the .ForMember()
overload with Func<>
and four parameters comes to the rescue:
public class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration(c => c.AddProfile<MyProfile>());
var mapper = config.CreateMapper();
var source = new SourceCollections
{
As = Enumerable.Range(0, 10).Select(i => new SourceA { Number = i }).ToList(),
Bs = Enumerable.Range(100, 10).Select(i => new SourceB { Number = i }).ToList(),
};
var dest = mapper.Map<DestinationCollections>(source);
foreach (var item in dest.Cs)
{
Console.WriteLine(item.Number);
}
}
}
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<SourceA, DestinationC>();
CreateMap<SourceB, DestinationC>();
CreateMap<SourceCollections, DestinationCollections>().ForMember(
d => d.Cs,
conf => conf.MapFrom((source, target, targetValue, context) =>
{
targetValue ??= new List<DestinationC>();
var valuesA = context.Mapper.Map<IReadOnlyList<DestinationC>>(source.As);
var valuesB = context.Mapper.Map<IReadOnlyList<DestinationC>>(source.Bs);
targetValue.AddRange(valuesA);
targetValue.AddRange(valuesB);
return targetValue;
}));
}
}
public class SourceCollections
{
public List<SourceA> As { get; set; }
public List<SourceB> Bs { get; set; }
}
public class DestinationCollections
{
public List<DestinationC> Cs { get; set; }
}
public class SourceA
{
public int Number { get; set; }
}
public class SourceB
{
public int Number { get; set; }
}
public class DestinationC
{
public int Number { get; set; }
}
Doing the reverse and split a source collection into several ones can be done in a similar way, but you need some criteria within the collection to decide which value should be converted into which type. You can handle both properties individually and each picks the desired items:
CreateMap<DestinationC, SourceA>();
CreateMap<DestinationC, SourceB>();
CreateMap<DestinationCollections, SourceCollections>()
.ForMember(
s => s.As,
conf => conf.MapFrom((source, target, targetValue, context) =>
{
targetValue ??= new List<SourceA>();
var candidates = source.Cs.Where(c => c.Number < 100);
var values = context.Mapper.Map<IReadOnlyList<SourceA>>(candidates);
targetValue.AddRange(values);
return targetValue;
}))
.ForMember(
s => s.Bs,
conf => conf.MapFrom((source, target, targetValue, context) =>
{
targetValue ??= new List<SourceB>();
var candidates = source.Cs.Where(c => c.Number >= 100);
var values = context.Mapper.Map<IReadOnlyList<SourceB>>(candidates);
targetValue.AddRange(values);
return targetValue;
}));