简体   繁体   中英

AutoMapper mapping unmapped properties to Dictionary / ExtensionData

How can I make AutoMapper to map missing unmapped properties to a dictionary inside the destination object? (Like ExtensionData during serialization)

Example:

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

Now I want the following result:

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

* EDIT *

In the end I am looking for a solution w/o reflection. Meaning: During setup/configuration/initialization reflection is allowed, but during the mapping itself, I do not want any delays caused by reflection.

* EDIT *

I am looking for a generic solution, just like the serializers.

There are a lot of possible solutions for your problem. I've create a custom value resolver for your property and it works perfectly:

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;
    }
}

How to use it?

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; }
}

I like Pawel's solution because is more generic. If you want something simpler but less generic you could initialize the mapper like this:

    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}}));
    });

You could combine Pawel's answer and Reflection.Emit to make it fast. Be aware that Reflection.Emit is not supported on all platforms (like iOS).

Unlike ExtensionData, this includes all property values from the source. I don't have an elegant solution to determine which properties were already mapped so I just provided an easy way to exclude certain properties.

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.");
    }
}

And use it like one of these

[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}}
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM