我正在尝试使用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; } }
我知道我可以通过这样明确地创建地图来实现:
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来做,但事实证明它比我预期的更具挑战性……
解决方法
如果您的用例与问题中的限制一样,那就是:
>从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,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,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,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,getter.Item2(srcEmbedded)); } return result; }; } private static IDictionary<string,object>>> CreateGetters(Type srcType) { return srcType .GetProperties(SrcFlags) .Where(p => p.CanRead) .ToDictionary(k => k.Name,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); 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,mappedFoo.Z); Console.WriteLine("Done"); Console.ReadLine(); } }