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

时间:2014-10-08 16:26:29

标签: c# automapper hal object-object-mapping

我正在尝试使用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; }
}

FooDTOHAL资源)

我知道我可以通过这样明确地创建地图来实现:

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进行此操作,但事实证明它比我预期的更具挑战性......

1 个答案:

答案 0 :(得分: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,我建议创建一个类似的地图构建器,它对类型匹配和嵌入属性映射进行编码。