2014-10-08 124 views
4

我試圖使用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做到這一點,但事實證明比我預想的更加困難...

+0

我不確定這是可能的,至少是你提出的方式。即使你可以創建一個泛型基類的映射,你也必須使用'.Include'來包含子類映射。 – 2014-10-08 18:00:59

回答

1

如果作爲問題提出你的使用情況是有限的,那就是:

  • 一個一從HalResource三通測繪的直POCOS實例(VS雙向映射)的同名的性質
  • 映射並鍵入
  • 你這裏提出
確切嵌入式結構

比自己設置一個考慮到這個結構的特定映射更有意義。如果我有一個非常狹義的需求,需要使用一些清晰的映射約定(而不是像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,我建議創建一個類似的地圖生成器,編碼兩種類型匹配和嵌入式屬性映射。

+0

感謝您的回答!你的代碼似乎工作得很好,但不幸的是它並不能滿足我所有的需求......我的對象實際上比你在例子中使用的要複雜一點:'Embedded'屬性通常包含其他的'HalResource'對象'BarDTO'和'BazDTO'在我的問題中),這也需要進行映射。當然,我可以改進你的代碼來處理這種情況,但是恐怕需要比手動配置AutoMapper更多的時間... – 2014-10-09 00:14:39

+0

現在我正在關注你最後的建議:我試圖動態配置AutoMapper基於程序集中的'HalResource'類型。 – 2014-10-09 00:15:59

+0

@ThomasLevesque感謝您的反饋。如果它是一個更復雜的嵌套結構,則可以調整自定義映射器,以便爲目標類型註冊多個源映射並迭代使用這些映射。但我同意,可能比專注於添加Mapper.CreateMap ().AfterMap((source,dest)=> Mapper.Map(source.Embedded,dest));將AutoMapper約定添加到每個構建器嵌入對象。 – Alex 2014-10-09 02:27:12