AutoMapper mapping unmapped properties to Dictionary / ExtensionData
Asked Answered
N

3

5

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.

Nellenelli answered 4/4, 2017 at 8:55 Comment(2)
I don't believe there's an inbuilt function to do this, however you could create a custom resolver that uses reflection to compare source and target properties and go from there.Yarrow
I am already wrestling with custom resolvers... But that is a pain. I cannot imagine I am the first who is walking into this, since serializers have this feature (which also map data from one "object" to another).Nellenelli
S
7

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; }
}
Selfemployed answered 4/4, 2017 at 9:51 Comment(4)
Nice solution. I choose AutoMapper to prevent read/writing data using reflection because it is slow. AutoMapper optimizes that process. I was hoping for a solution that in the end, I have a fully optimized mapping process w/o any significant delays. Anwyay... +1 for you.Nellenelli
I edited my question, so you answer is not really fitting the criteria anymore. But again: A very good start for me.Nellenelli
AutoMapper is using Reflection. msdn.microsoft.com/en-us/library/… I'm not sure if there is possibility to detect unmapped properties without reflection.Selfemployed
Yes.. it is using reflection during the construction/configuration fase. So it is using reflection during the creation of the mapper. When the once the mapper is constructed and used during mappings, no reflection is used.Nellenelli
S
5

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}}));
    });
Sella answered 4/4, 2017 at 11:52 Comment(1)
Thanx. Yes I am looking for a generic solution. Still +1.Nellenelli
T
0

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}}
    }
}
Tko answered 11/2, 2022 at 21:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.