5

如何使 AutoMapper 将缺少的未映射属性映射到目标对象内的字典?(如序列化期间的ExtensionData

例子:

class Source
{
    public int A {get;set;} 
    public int B {get;set;} 
    public int C {get;set;} 
}

class Destination
{
    public int A {get;set;}
    public Dictionary<string, object> D {get;set;}
}

Source s = new Source { A = 1, B = 2, C = 3 };
Destination d = ... // Mapping code

现在我想要以下结果:

d.A ==> 1
d.D ==> {{ "B", 2 }, { "C", 3 }}

* 编辑 *

最后,我正在寻找没有反射的解决方案。含义:在设置/配置/初始化期间允许反射,但在映射本身期间,我不希望由反射引起任何延迟。

* 编辑 *

我正在寻找一个通用的解决方案,就像序列化程序一样。

4

3 回答 3

7

您的问题有很多可能的解决方案。我已经为您的属性创建了一个自定义值解析器,它运行良好:

public class CustomResolver : IValueResolver<Source, Destination, Dictionary<string, object>>
{
    public Dictionary<string, object> Resolve(Source source, Destination destination, Dictionary<string, object> destMember, ResolutionContext context)
    {
        destMember = new Dictionary<string, object>();

        var flags = BindingFlags.Public | BindingFlags.Instance;
        var sourceProperties = typeof(Source).GetProperties(flags);

        foreach (var property in sourceProperties)
        {
            if (typeof(Destination).GetProperty(property.Name, flags) == null)
            {
                destMember.Add(property.Name, property.GetValue(source));
            }
        }

        return destMember;
    }
}

如何使用它?

static void Main(string[] args)
{
    Mapper.Initialize(cfg => {
        cfg.CreateMap<Source, Destination>()
            .ForMember(dest => dest.D, opt => opt.ResolveUsing<CustomResolver>());
    });

    var source = new Source { A = 1, B = 2, C = 3 };

    var result = Mapper.Map<Source, Destination>(source);
}

public class Source
{
    public int A { get; set; }
    public int B { get; set; }
    public int C { get; set; }
}

public class Destination
{
    public int A { get; set; }
    public Dictionary<string, object> D { get; set; }
}
于 2017-04-04T09:51:04.903 回答
4

我喜欢 Pawel 的解决方案,因为它更通用。如果你想要一些更简单但不太通用的东西,你可以像这样初始化映射器:

    Mapper.Initialize(cfg => {
                          cfg.CreateMap<Source, Destination>()
                              .ForMember(dest => dest.D, 
                                         opt => opt.MapFrom(r => new Dictionary<string,object>(){{ "B", r.B},{ "C", r.C}}));
    });
于 2017-04-04T11:52:40.013 回答
0

您可以将 Pawel 的答案和 Reflection.Emit 结合起来,让它变得更快。请注意,并非所有平台(如 iOS)都支持 Reflection.Emit。

与 ExtensionData 不同,这包括源中的所有属性值。我没有一个优雅的解决方案来确定哪些属性已经被映射,所以我只是提供了一种简单的方法来排除某些属性。

public class PropertyDictionaryResolver<TSource> : IValueResolver<TSource, object, Dictionary<string, object>>
{
    private static readonly PropertyInfo[] Properties = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance);
    private static readonly ConcurrentDictionary<PropertyInfo, Func<TSource, object>> GetterCache = new ConcurrentDictionary<PropertyInfo, Func<TSource, object>>();

    public HashSet<MemberInfo> ExcludedProperties;

    public PropertyDictionaryResolver()
    {
        ExcludedProperties = new HashSet<MemberInfo>();
    }

    public PropertyDictionaryResolver(Expression<Func<TSource, object>> excludeMembers)
    {
        var members = ExtractMembers(excludeMembers);
        ExcludedProperties = new HashSet<MemberInfo>(members);
    }

    public Dictionary<string, object> Resolve(TSource source, object destination, Dictionary<string, object> existing, ResolutionContext context)
    {
        var destMember = new Dictionary<string, object>();

        foreach (var property in Properties)
        {
            if (ExcludedProperties.Contains(property)) continue;

            var exp = GetOrCreateExpression(property);
            var value = exp(source);
            if (value != null)
            {
                destMember.Add(property.Name, value);
            }
        }

        return destMember;
    }

    /// <summary>
    /// Creates and compiles a getter function for a property
    /// </summary>
    /// <param name="propInfo"></param>
    /// <returns></returns>
    public Func<TSource, object> GetOrCreateExpression(PropertyInfo propInfo)
    {
        if (GetterCache.TryGetValue(propInfo, out var existing))
        {
            return existing;
        }

        var parameter = Expression.Parameter(typeof(TSource));
        var property = Expression.Property(parameter, propInfo);
        var conversion = Expression.Convert(property, typeof(object));
        var lambda = Expression.Lambda<Func<TSource, object>>(conversion, parameter);

        existing = lambda.Compile();
        GetterCache.TryAdd(propInfo, existing);
        return existing;
    }

    /// <summary>
    /// Pull the used MemberInfo out of a simple expression.  Supports the following expression types only:
    /// s => s.Prop1
    /// s => new { s.Prop1, s.Prop2 }
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="expression"></param>
    /// <returns></returns>
    public static IEnumerable<MemberInfo> ExtractMembers<T>(Expression<Func<T, object>> expression)
    {
        if (expression == null)
        {
            throw new ArgumentNullException(nameof(expression));
        }

        switch (expression.Body)
        {
            case MemberExpression memberExpression:
                yield return memberExpression.Member;
                yield break;

            // s => s.BarFromBaseType 
            case UnaryExpression convertExpression:
                if (convertExpression.Operand is MemberExpression exp)
                {
                    yield return exp.Member;
                }

                yield break;

            // s => new { s.Foo, s.Bar }
            case NewExpression newExpression:
                if (newExpression.Arguments.Count == 0)
                {
                    yield break;
                }

                foreach (var argument in newExpression.Arguments.OfType<MemberExpression>())
                {
                    yield return argument.Member;
                }

                yield break;
        }

        throw new NotImplementedException("Unrecognized lambda expression.");
    }
}

并像其中之一一样使用它

[TestClass]
public class Examples
{
    [TestMethod]
    public void AllProperties()
    {
        var mapper = new Mapper(new MapperConfiguration(p =>
        {
            p.CreateMap<Source, Destination>()
                .ForMember(x => x.A, cfg => cfg.MapFrom(x => x.A))
                .ForMember(x => x.D, cfg => cfg.MapFrom<PropertyDictionaryResolver<Source>>());

        }));

        var source = new Source { A = 1, B = 2, C = 3 };
        var d = mapper.Map<Destination>(source);
        // {"A":1,"D":{"A":1,"B":2,"C":3}}
    }

    [TestMethod]
    public void ExcludeSingleProperty()
    {
        var mapper = new Mapper(new MapperConfiguration(p =>
        {
            p.CreateMap<Source, Destination>()
            .ForMember(x => x.A, cfg => cfg.MapFrom(x => x.A))
            .ForMember(x => x.D, cfg => cfg.MapFrom(new PropertyDictionaryResolver<Source>(x => x.A)));

        }));

        var source = new Source { A = 1, B = 2, C = 3 };
        var d = mapper.Map<Destination>(source);
        // {"A":1,"D":{"B":2,"C":3}}
    }

    [TestMethod]
    public void ExcludeMultipleProperties()
    {
        var mapper = new Mapper(new MapperConfiguration(p =>
        {
            p.CreateMap<Source, Destination>()
                .ForMember(x => x.A, cfg => cfg.MapFrom(x => x.A))
                .ForMember(x => x.D, cfg => cfg.MapFrom(new PropertyDictionaryResolver<Source>(x => new
                {
                    x.A, 
                    x.B
                })));

        }));

        var source = new Source { A = 1, B = 2, C = 3 };
        var d = mapper.Map<Destination>(source);
        // {"A":1,"D":{"C":3}}
    }
}
于 2022-02-11T21:06:26.930 回答