14
\$\begingroup\$

I am working on a helper method that maps properties from an ExpandoObject to a user supplied object and was wondering if the code could be cleaned up or made any more efficient. It currently has the correct behaviour from a simple test.

public static class Mapper
{
    public static void Map<T>(ExpandoObject source, T destination)
    {
        IDictionary<string, object> dict = source;
        var type = destination.GetType();

        foreach (var prop in type.GetProperties())
        {
            var lower = prop.Name.ToLower();
            var key = dict.Keys.SingleOrDefault(k => k.ToLower() == lower);

            if (key != null)
            {
                prop.SetValue(destination, dict[key], null);
            }
        }
    }
}

Full test can be seen here. There is currently no type checking. Would this be next to add?

\$\endgroup\$
1
  • \$\begingroup\$ If the ExpandoObject's are coming from JSON deserialization, might be simpler if you instead convert to JObjects. As seen here. \$\endgroup\$ Mar 11, 2021 at 0:05

2 Answers 2

9
\$\begingroup\$

I've come up with a few changes that should actually speed it up.

// By using a generic class we can take advantage
// of the fact that .NET will create a new generic type
// for each type T. This allows us to avoid creating
// a dictionary of Dictionary<string, PropertyInfo>
// for each type T. We also avoid the need for the 
// lock statement with every call to Map.
public static class Mapper<T>
    // We can only use reference types
    where T : class
{
    private static readonly Dictionary<string, PropertyInfo> _propertyMap;

    static Mapper()
    {
        // At this point we can convert each
        // property name to lower case so we avoid 
        // creating a new string more than once.
        _propertyMap = 
            typeof(T)
            .GetProperties()
            .ToDictionary(
                p => p.Name.ToLower(), 
                p => p
            );
    }

    public static void Map(ExpandoObject source, T destination)
    {
        // Might as well take care of null references early.
        if (source == null)
            throw new ArgumentNullException("source");
        if (destination == null)
            throw new ArgumentNullException("destination");

        // By iterating the KeyValuePair<string, object> of
        // source we can avoid manually searching the keys of
        // source as we see in your original code.
        foreach (var kv in source)
        {
            PropertyInfo p;
            if (_propertyMap.TryGetValue(kv.Key.ToLower(), out p))
            {
                var propType = p.PropertyType;
                if (kv.Value == null)
                {
                    if (!propType.IsByRef && propType.Name != "Nullable`1")
                    {
                        // Throw if type is a value type 
                        // but not Nullable<>
                        throw new ArgumentException("not nullable");
                    }
                }
                else if (kv.Value.GetType() != propType)
                {
                    // You could make this a bit less strict 
                    // but I don't recommend it.
                    throw new ArgumentException("type mismatch");
                }
                p.SetValue(destination, kv.Value, null);
            }
        }
    }
}
\$\endgroup\$
3
  • \$\begingroup\$ I'd advise you to check out Fasterflect (fasterflect.codeplex.com), which is a library to make reflection tasks easier (through an abstraction over the classic reflection API) and much faster (using dynamic code generation). Also worth mentioning is AutoMapper, which is a library dedicated just to the task of mapping between objects. \$\endgroup\$ Mar 2, 2011 at 14:28
  • 1
    \$\begingroup\$ May be ok for now but needs more work to handle nested types. \$\endgroup\$
    – nawfal
    Jul 19, 2014 at 8:59
  • 1
    \$\begingroup\$ Be aware that this solution breaks on polymorphism. Suppose T is abstract and you provide an instance of V (V : T) to the mapper of type T, the properties declared at V are not mapped. Is this as designed? \$\endgroup\$
    – dfhwze
    Jun 27, 2019 at 19:50
4
\$\begingroup\$

Review

  • Generic type definition T is redundant.
  • Looping the ExpandoObject is better optimized then looping the destination's properties, because you get the items as key-value pair in a single pass.
  • Fetch only publicly accessible setter properties to avoid nasty exceptions on property.SetValue.
  • Don't be too strict on matching the source and destination's types. Take advantage of the built-in type converter API Convert.ChangeType.

Proposed Alternative

TypeExtension allows for a changing an instance's type.

public static class TypeExtension
{
    public static bool IsNullable(this Type type)
    {
        type = type ?? throw new ArgumentNullException(nameof(type));
        return type.IsGenericType 
            && type.GetGenericTypeDefinition().Equals(typeof(Nullable<>));
    }

    public static bool IsNullAssignable(this Type type)
    {
        type = type ?? throw new ArgumentNullException(nameof(type));
        return type.IsNullable() || !type.IsValueType;
    }

    public static object ChangeType(this Type type, object instance)
    {
        type = type ?? throw new ArgumentNullException(nameof(type));
        if (instance == null)
        {
            if (!type.IsNullAssignable())
            {
                throw new InvalidCastException($"{type.FullName} is not null-assignable");
            }
            return null;
        }
        if (type.IsNullable())
        {
            type = Nullable.GetUnderlyingType(type);
        }
        return Convert.ChangeType(instance, type);
    }
}

Mapper could be optimized, made more robust and better suited to map between different types.

public static class Mapper
{
    public static void Map(ExpandoObject source, object destination)
    {
        source = source ?? throw new ArgumentNullException(nameof(source));
        destination = destination ?? throw new ArgumentNullException(nameof(destination));

        string normalizeName(string name) => name.ToLowerInvariant();

        IDictionary<string, object> dict = source;
        var type = destination.GetType();

        var setters = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .Where(p => p.CanWrite && p.GetSetMethod() != null)
            .ToDictionary(p => normalizeName(p.Name));

        foreach (var item in dict)
        {
            if (setters.TryGetValue(normalizeName(item.Key), out var setter))
            {
                var value = setter.PropertyType.ChangeType(item.Value);
                setter.SetValue(destination, value);
            }
        }
    }
}

Test Case:

public class Point
{
    public int? X { get; set; }
    public double Y { get; set; }
}

static void Main(string[] args)
{
    dynamic source = new ExpandoObject();
    source.X = 0;
    source.Y = 0m;

    var destination = new Point
    {
        X = 1,
        Y = 1d
    };

    Mapper.Map(source, destination);

    Console.WriteLine(destination.X);
    Console.WriteLine(destination.Y);
    Console.ReadKey();
}
\$\endgroup\$
7
  • \$\begingroup\$ My gut tells me that this is a xy-problem becuase I cannot think of any reasonable use-case where one would want to use such mapping. OP is probably parsing json into dynamic instead of T thus they need to fix that with this mapper. \$\endgroup\$
    – t3chb0t
    Jun 27, 2019 at 17:39
  • \$\begingroup\$ @t3chb0t I can see a use case in WPF when binding a view to an ExpandoObject and mapping further to some DTO. The ExpandoObject can be usefull for a table with a dynamic configurable set of columns. \$\endgroup\$
    – dfhwze
    Jun 27, 2019 at 17:41
  • 1
    \$\begingroup\$ @TomBell do you remember what you needed this for? \$\endgroup\$
    – t3chb0t
    Jun 27, 2019 at 17:42
  • \$\begingroup\$ I'm answering this question because I feel the accepted answer has a major flaw using the properties of T. The instance can be of a derived type, breaking the mapping. \$\endgroup\$
    – dfhwze
    Jun 27, 2019 at 17:43
  • 1
    \$\begingroup\$ sure, the review is fine, +1 as always ;-P but I'm curious about the real use case... \$\endgroup\$
    – t3chb0t
    Jun 27, 2019 at 17:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.