展平嵌套对象以将其属性映射到目标对象

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&lt;TEmbedded&gt;的类全局配置映射,使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 对象( BarDTOBazDTO 在我的问题中),也需要映射。当然,我可能会改进你的代码来处理这种情况,但我担心它会比手动配置 AutoMapper 花费更多的时间...... 现在我正在遵循您的最后一个建议:我正在尝试根据程序集中存在的 HalResource 类型动态配置 AutoMapper。 @ThomasLevesque 感谢您的反馈。如果它是一个比您更复杂的嵌套结构,您可以调整自定义映射器,以便它为目标类型注册多个源映射并迭代地使用它们。但我同意这可能比专注于将 Mapper.CreateMap&lt;FooDTO, Foo&gt;().AfterMap((source, dest) =&gt; Mapper.Map(source.Embedded, dest)); AutoMapper 约定添加到每个嵌入对象的构建器要付出更多努力。

以上是关于展平嵌套对象以将其属性映射到目标对象的主要内容,如果未能解决你的问题,请参考以下文章

映射/挖掘嵌套在对象中的数组 - JavaScript

将展平对象的属性映射为集合

使用 jq 为 JSON 对象的嵌套数组中的属性展平数组

AutoMapper - 试图将我的对象属性展平为字符串但出现错误

使用唯一键展平/规范化深度嵌套的对象

mybatis是如何将sql执行结果封装为目标对象并返回的?有哪些映射形式