展平嵌套对象以将其属性映射到目标对象
Posted
技术标签:
【中文标题】展平嵌套对象以将其属性映射到目标对象【英文标题】:Flatten a nested object to map its properties to the target object 【发布时间】:2014-12-03 09:40:29 【问题描述】:我正在尝试使用 AutoMapper 来映射这样的类:
class FooDTO
public int X get; set;
public EmbeddedDTO Embedded get; set;
public class EmbeddedDTO
public BarDTO Y get; set;
public BazDTO Z get; set;
像这样的类:
class Foo
public int X get; set;
public Bar Y get; set;
public Baz Z get; set;
(FooDTO
是 HAL 资源)
我知道我可以通过像这样明确地创建地图来做到这一点:
Mapper.CreateMap<FooDTO, Foo>()
.ForMember(f => f.Y, c => c.MapFrom(f => f.Embedded.Y))
.ForMember(f => f.Z, c => c.MapFrom(f => f.Embedded.Z));
或者甚至使用这样的技巧:
Mapper.CreateMap<FooDTO, Foo>()
.AfterMap((source, dest) => Mapper.Map(source.Embedded, dest));
但问题是我将有许多类似的 HAL 资源要映射,我宁愿不必单独配置每一个。我实际上有一个看起来像这样的通用对象模型:
class HalResource
[JsonProperty("_links")]
public IDictionary<string, HalLink> Links get; set;
class HalResource<TEmbedded> : HalResource
[JsonProperty("_embedded")]
public TEmbedded Embedded get; set;
class HalLink
[JsonProperty("href")]
public string Href get; set;
使用这个模型,FooDTO
类实际上是这样声明的
class FooDTO : HalResource<FooDTO.EmbeddedDTO>
public int X get; set;
public class EmbeddedDTO
public int Y get; set;
public int Z get; set;
有没有办法为所有继承HalResource<TEmbedded>
的类全局配置映射,使DTO的Embedded
属性的属性直接映射到目标对象?我试过使用自定义 IObjectMapper
来完成,但事实证明它比我预期的更具挑战性......
【问题讨论】:
我不确定这是否可行,至少您提出的方式是这样。即使您可以为通用基类创建映射,您也必须使用.Include
来包含子类映射。
【参考方案1】:
如果您的用例与问题中所述的一样有限,那就是:
从 HalResource 派生实例到直接 POCOS 的单向映射(相对于双向映射) 同名同类型属性的映射 您在此处展示的确切嵌入式结构比自己设置一个考虑到这种结构的特定映射可能更有意义。如果我对使用一些明确的映射约定(而不是依赖于诸如 AutoMapper 之类的通用映射器)进行映射的定义非常狭窄,我倾向于这样做。为此,我有一些构建块,我倾向于在不同的上下文中重用它们。我整理了一个映射器,该映射器适用于您从这些构建块中描述的问题,如下所示:
public class Mapper
private const BindingFlags DestConstructorFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
private const BindingFlags DestFlags = BindingFlags.Instance | BindingFlags.Public;
private const BindingFlags SrcFlags = BindingFlags.Instance | BindingFlags.Public;
private static readonly object[] NoArgs = new object[0];
private static readonly Type GenericEmbeddedSourceType = typeof(HalResource<>);
private readonly Dictionary<Type, Func<object, object>> _oneWayMap = new Dictionary<Type, Func<object, object>>();
public void CreateMap<TDestination, TSource>()
where TDestination : class
where TSource : HalResource
CreateMap(typeof(TDestination), typeof(TSource));
public void CreateMap(Type destType, Type srcType)
_oneWayMap[srcType] = InternalCreateMapper(destType, srcType);
public object Map<TSource>(TSource toMap) where TSource : HalResource
var mapper = default(Func<object, object>);
if (!_oneWayMap.TryGetValue(typeof(TSource), out mapper))
throw new KeyNotFoundException(string.Format("No mapping for 0 is defined.", typeof(TSource)));
return mapper(toMap);
public TDestination Map<TDestination, TSource>(TSource toMap)
where TDestination : class
where TSource : HalResource
var converted = Map(toMap);
if (converted != null && !typeof(TDestination).IsAssignableFrom(converted.GetType()))
throw new InvalidOperationException(string.Format("No mapping from type 0 to type 1 has been configured.", typeof(TSource), typeof(TDestination)));
return (TDestination)converted;
public void Clear()
_oneWayMap.Clear();
private static Func<object, object> InternalCreateMapper(Type destType, Type srcType)
// Destination specific constructor + setter map.
var destConstructor = BuildConstructor(destType.GetConstructor(DestConstructorFlags, null, Type.EmptyTypes, null));
var destSetters = destType
.GetProperties(DestFlags)
.Where(p => p.CanWrite)
.ToDictionary(k => k.Name, v => Tuple.Create(v.PropertyType, BuildSetter(v)));
// Source specific getter maps
var srcPrimPropGetters = CreateGetters(srcType);
var srcEmbeddedGetter = default(Func<object, object>);
var srcEmbeddedPropGetters = default(IDictionary<string, Tuple<Type, Func<object, object>>>);
var baseType = srcType.BaseType;
while (baseType != null && baseType != typeof(object))
if (baseType.IsGenericType && GenericEmbeddedSourceType.IsAssignableFrom(baseType.GetGenericTypeDefinition()))
var genericParamType = baseType.GetGenericArguments()[0];
if (srcPrimPropGetters.Any(g => g.Value.Item1.Equals(genericParamType)))
var entry = srcPrimPropGetters.First(g => g.Value.Item1.Equals(genericParamType));
srcPrimPropGetters.Remove(entry.Key);
srcEmbeddedGetter = entry.Value.Item2;
srcEmbeddedPropGetters = CreateGetters(entry.Value.Item1);
break;
baseType = baseType.BaseType;
// Build mapper delegate function.
return (src) =>
var result = destConstructor(NoArgs);
var srcEmbedded = srcEmbeddedGetter != null ? srcEmbeddedGetter(src) : null;
foreach (var setter in destSetters)
var getter = default(Tuple<Type, Func<object, object>>);
if (srcPrimPropGetters.TryGetValue(setter.Key, out getter) && setter.Value.Item1.IsAssignableFrom(getter.Item1))
setter.Value.Item2(result, getter.Item2(src));
else if (srcEmbeddedPropGetters.TryGetValue(setter.Key, out getter) && setter.Value.Item1.IsAssignableFrom(getter.Item1))
setter.Value.Item2(result, getter.Item2(srcEmbedded));
return result;
;
private static IDictionary<string, Tuple<Type, Func<object, object>>> CreateGetters(Type srcType)
return srcType
.GetProperties(SrcFlags)
.Where(p => p.CanRead)
.ToDictionary(k => k.Name, v => Tuple.Create(v.PropertyType, BuildGetter(v)));
private static Func<object[], object> BuildConstructor(ConstructorInfo constructorInfo)
var param = Expression.Parameter(typeof(object[]), "args");
var argsExp = constructorInfo.GetParameters()
.Select((p, i) => Expression.Convert(Expression.ArrayIndex(param, Expression.Constant(i)), p.ParameterType))
.ToArray();
return Expression.Lambda<Func<object[], object>>(Expression.New(constructorInfo, argsExp), param).Compile();
private static Func<object, object> BuildGetter(PropertyInfo propertyInfo)
var instance = Expression.Parameter(typeof(object), "instance");
var instanceCast = propertyInfo.DeclaringType.IsValueType
? Expression.Convert(instance, propertyInfo.DeclaringType)
: Expression.TypeAs(instance, propertyInfo.DeclaringType);
var propertyCast = Expression.TypeAs(Expression.Property(instanceCast, propertyInfo), typeof(object));
return Expression.Lambda<Func<object, object>>(propertyCast, instance).Compile();
private static Action<object, object> BuildSetter(PropertyInfo propertyInfo)
var setMethodInfo = propertyInfo.GetSetMethod(true);
var instance = Expression.Parameter(typeof(object), "instance");
var value = Expression.Parameter(typeof(object), "value");
var instanceCast = propertyInfo.DeclaringType.IsValueType
? Expression.Convert(instance, propertyInfo.DeclaringType)
: Expression.TypeAs(instance, propertyInfo.DeclaringType);
var call = Expression.Call(instanceCast, setMethodInfo, Expression.Convert(value, propertyInfo.PropertyType));
return Expression.Lambda<Action<object, object>>(call, instance, value).Compile();
可以执行一些优化,但性能可能足以解决大多数问题。然后可以这样使用:
public abstract class HalResource
public IDictionary<string, HalLink> Links get; set;
public abstract class HalResource<TEmbedded> : HalResource
public TEmbedded Embedded get; set;
public class HalLink
public string Href get; set;
public class FooDTO : HalResource<FooDTO.EmbeddedDTO>
public int X get; set;
public class EmbeddedDTO
public int Y get; set;
public int Z get; set;
public class MyMappedFoo
public int X get; set;
public int Y get; set;
public int Z get; set;
class Program
public static void Main(params string[] args)
// Configure mapper manually
var mapper = new Mapper();
mapper.CreateMap<MyMappedFoo, FooDTO>();
var myDTO = new FooDTO
X = 10,
Embedded = new FooDTO.EmbeddedDTO Y = 5, Z = 9
;
var mappedFoo = mapper.Map<MyMappedFoo, FooDTO>(myDTO);
Console.WriteLine("X = 0, Y = 1, Z = 2", mappedFoo.X, mappedFoo.Y, mappedFoo.Z);
Console.WriteLine("Done");
Console.ReadLine();
如果您的源类型和目标类型可以通过约定发现,您可以更进一步,让对这些约定进行编码的构建器填充映射,如下例所示(同样不是最佳实现,但用于说明点):
public static class ByConventionMapBuilder
public static Func<IEnumerable<Type>> DestinationTypesProvider = DefaultDestTypesProvider;
public static Func<IEnumerable<Type>> SourceTypesProvider = DefaultSourceTypesProvider;
public static Func<Type, Type, bool> TypeMatcher = DefaultTypeMatcher;
public static Mapper Build()
var mapper = new Mapper();
var sourceTypes = SourceTypesProvider().ToList();
var destTypes = DestinationTypesProvider();
foreach (var destCandidateType in destTypes)
var match = sourceTypes.FirstOrDefault(t => TypeMatcher(t, destCandidateType));
if (match != null)
mapper.CreateMap(destCandidateType, match);
sourceTypes.Remove(match);
return mapper;
public static IEnumerable<Type> TypesFromAssembliesWhere(Func<IEnumerable<Assembly>> assembliesProvider, Predicate<Type> matches)
foreach (var a in assembliesProvider())
foreach (var t in a.GetTypes())
if (matches(t))
yield return t;
private static IEnumerable<Type> DefaultDestTypesProvider()
return TypesFromAssembliesWhere(
() => new[] Assembly.GetExecutingAssembly() ,
t => t.IsClass && !t.IsAbstract && !t.Name.EndsWith("DTO"));
private static IEnumerable<Type> DefaultSourceTypesProvider()
return TypesFromAssembliesWhere(
() => new[] Assembly.GetExecutingAssembly() ,
t => typeof(HalResource).IsAssignableFrom(t) && !t.IsAbstract && t.Name.EndsWith("DTO"));
private static bool DefaultTypeMatcher(Type srcType, Type destType)
var stn = srcType.Name;
return (stn.Length > 3 && stn.EndsWith("DTO") && destType.Name.EndsWith(stn.Substring(0, stn.Length - 3)));
class Program
public static void Main(params string[] args)
// Configure mapper by type scanning & convention matching
var mapper = ByConventionMapBuilder.Build();
var myDTO = new FooDTO
X = 10,
Embedded = new FooDTO.EmbeddedDTO Y = 5, Z = 9
;
var mappedFoo = mapper.Map<MyMappedFoo, FooDTO>(myDTO);
Console.WriteLine("X = 0, Y = 1, Z = 2", mappedFoo.X, mappedFoo.Y, mappedFoo.Z);
Console.WriteLine("Done");
Console.ReadLine();
如果您有其他原因想要继续使用 AutoMapper,我建议您创建一个类似的地图构建器,对类型匹配和嵌入式属性映射进行编码。
【讨论】:
感谢您的回答!您的代码似乎运行良好,但不幸的是它并不能满足我的所有需求......我的对象实际上比您在示例中使用的要复杂一些:Embedded
属性通常包含其他 HalResource
对象( BarDTO
和 BazDTO
在我的问题中),也需要映射。当然,我可能会改进你的代码来处理这种情况,但我担心它会比手动配置 AutoMapper 花费更多的时间......
现在我正在遵循您的最后一个建议:我正在尝试根据程序集中存在的 HalResource
类型动态配置 AutoMapper。
@ThomasLevesque 感谢您的反馈。如果它是一个比您更复杂的嵌套结构,您可以调整自定义映射器,以便它为目标类型注册多个源映射并迭代地使用它们。但我同意这可能比专注于将 Mapper.CreateMap<FooDTO, Foo>().AfterMap((source, dest) => Mapper.Map(source.Embedded, dest));
AutoMapper 约定添加到每个嵌入对象的构建器要付出更多努力。以上是关于展平嵌套对象以将其属性映射到目标对象的主要内容,如果未能解决你的问题,请参考以下文章