So I have something besides the usual DTO to business mapper and I'm trying to map them with minimal amount of mapping code.
Setup
public class Target {
public string propA { get; set; }
public string propB { get; set; }
public string propC { get; set; }
public string propD { get; set; }
public string propE { get; set; }
public List<KeyValuePair> Tokens { get; set; }
}
public class Source {
public SomeClass SomeClass { get; set; }
public AnotherClass AnotherClass { get; set; }
}
public class SomeClass {
public string propA { get; set; }
public string propB { get; set; }
public string propDifferent { get; set; }
public List<KeyValuePair> Tokens { get; set; }
}
public class AnotherClass {
public string propC { get; set; }
public string propD { get; set; }
public List<KeyValuePair> Tokens { get; set; }
}
Mapper Config
Mapper.CreateMap<SomeClass, Target>()
.ForMember(dest => dest.propE, opt => opt.MapFrom(src => src.propDifferent));
Mapper.CreateMap<AnotherClass, Target>();
Mapper.CreateMap<Source, Target>()
.ForMember(dest => dest, opt => opt.MapFrom(src => src.SomeClass))
.ForMember(dest => dest, opt => opt.MapFrom(src => src.AnotherClass));
Doing this throws
Error: AutoMapper.AutoMapperConfigurationException: Custom configuration for members is only supported for top-level individual members on a type.
And I also need to take AnotherClass.Tokens
, SomeClass.Tokens
and add it to Target.Tokens
.
I know I can use .ConvertUsing
but then I have to define mapping for every property and I lose the advantage of convention based mapping for matching properties.
Is there any other way of achieving this (other than .ConvertUsing
or mapping every property by hand)?
If not via Automapper, is it doable via EmitMapper? I guess adding to Tokens list is probably doable via EmitMapper's PostProcessing
.
Update
After a bit of hacking, I found a way:
public static IMappingExpression<TSource, TDestination> FlattenNested<TSource, TNestedSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
{
var sourceType = typeof(TNestedSource);
var destinationType = typeof(TDestination);
var sourceProperties = sourceType.GetProperties().ToDictionary(x => x.Name.ToLowerInvariant());
var childPropName = typeof (TSource).GetProperties().First(x => x.PropertyType == sourceType).Name;
var mappableProperties = destinationType.GetProperties()
.Where(p => sourceProperties.ContainsKey(p.Name.ToLowerInvariant()) &&
sourceProperties[p.Name.ToLowerInvariant()].PropertyType ==
p.PropertyType)
.Select(p => new {DestProperty = p.Name, SrcProperty = sourceProperties[p.Name.ToLowerInvariant()].Name});
foreach (var property in mappableProperties)
{
expression.ForMember(property.DestProperty,
opt => opt.MapFrom(src => src.GetPropertyValue(childPropName).GetPropertyValue(property.SrcProperty)));
}
return expression;
}
Note: I do the Name.ToLowerInvariant()
to be able to match AccountID
-> AccountId
and similar.
Usage
AutoMapper.Mapper.CreateMap<Source, Target>()
.FlattenNested<Source, SomeClass, Target>()
.FlattenNested<Source, AnotherClass, Target>()
.ForMember(dest => dest.propE, opt => opt.MapFrom(src => src.propDifferent));
I spotted some other properties in IMappingExpression
that I maybe able to use and cleanup a lot of this. Will update as I find them.