2014-10-08 3 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 является a 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>, так что свойства свойства DTC Embedded отображаются непосредственно на целевой объект? Я пытался сделать это с пользовательской IObjectMapper, но она оказалась более сложной, чем я ожидал ...

+0

Я не уверен, что это возможно, по крайней мере, так, как вы его представили. Даже если вы * можете * создать сопоставление для базового базового класса, вам нужно будет использовать '.Include' для включения сопоставлений дочерних классов. –

ответ

1

Если случай использования столь ограничены, как представлено в этом вопросе, то есть:

  • Один -way отображение 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 ... –

+0

Сейчас я следую вашему последнему предложению: я пытаюсь настроить AutoMapper динамически основанные на типах HalResource, присутствующих в сборке. –

+0

@ThomasLevesque Спасибо за ваши отзывы. Если это более сложная вложенная структура, чем вы могли бы либо адаптировать настраиваемый сопоставитель таким образом, чтобы он регистрировал множественные сопоставления источников для типа назначения и использовал их итеративно. Но я согласен, что может быть больше усилий, чем сосредоточиться на добавлении «Mapper.CreateMap () .AfterMap ((source, dest) => Mapper.Map (source.Embedded, dest));« Соглашение AutoMapper для строителя для каждого встроенный объект. – Alex

Смежные вопросы