Recursively Mapping ExpandoObject
Asked Answered
V

1

7

In my application i have to use ExpandoObject in order to create/delete properties during the runtime; However, i have to map the returned ExpandoObject of a function to the corresponding object/class. So i have came up with a small Mapper that does the job but with 3 problems:

  1. It does not recursively map the inner objects of the ExpandoObject as supposed.
  2. When i try to map int to a Nullable simply it will throw a type mismatch because i can't find a way to detect and cast it properly.
  3. Fields can't be mapped public string Property;.

Code:

I- Implementation:

public static class Mapper<T> where T : class
{
    #region Properties

    private static readonly Dictionary<string, PropertyInfo> PropertyMap;

    #endregion

    #region Ctor

    static Mapper() { PropertyMap = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).ToDictionary(p => p.Name.ToLower(), p => p); }

    #endregion

    #region Methods

    public static void Map(ExpandoObject source, T destination)
    {
        if (source == null)
            throw new ArgumentNullException("source");
        if (destination == null)
            throw new ArgumentNullException("destination");

        foreach (var kv in source)
        {
            PropertyInfo p;
            if (PropertyMap.TryGetValue(kv.Key.ToLower(), out p))
            {
                Type propType = p.PropertyType;
                if (kv.Value == null)
                {
                    if (!propType.IsByRef && propType.Name != "Nullable`1")
                    {
                        throw new ArgumentException("not nullable");
                    }
                }
                else if (kv.Value.GetType() != propType)
                {
                    throw new ArgumentException("type mismatch");
                }
                p.SetValue(destination, kv.Value, null);
            }
        }
    }

    #endregion
}

II: Usage:

public static void Main()
{
    Class c = new Class();
    dynamic o = new ExpandoObject();
    o.Name = "Carl";
    o.Level = 7;
    o.Inner = new InnerClass
              {
                      Name = "Inner Carl",
                      Level = 10
              };

    Mapper<Class>.Map(o, c);

    Console.Read();
}

internal class Class
{
    public string Name { get; set; }
    public int? Level { get; set; }
    public InnerClass Inner { get; set; }
    public string Property;
}

internal class InnerClass
{
    public string Name { get; set; }
    public int? Level { get; set; }
}
Verify answered 22/10, 2013 at 21:59 Comment(0)
S
6

3- If the property is formated like this public string Property; the get properties does not get it.

Oh, that's not a property, that's a field. If you want consider fields as well.

static Mapper()
{
    PropertyMap = typeof(T).GetProperties(BindingFlags.Public |
                                              BindingFlags.NonPublic |
                                              BindingFlags.Instance)
                                              .ToDictionary(p => p.Name.ToLower(), p => p);

    FieldMap = typeof(T).GetFields(BindingFlags.Public |
                                                BindingFlags.NonPublic |
                                                BindingFlags.Instance)
                                                .ToDictionary(f => f.Name.ToLower(), f => f);
}

2- When i try to map int to a Nullable simply it will throw a type mismatch because i can't find a way to detect and cast it properly.

Why check for Nullable type, let reflection figure it out. If value is valid, it will be assigned.

public static void Map(ExpandoObject source, T destination)
{
    if (source == null)
        throw new ArgumentNullException("source");
    if (destination == null)
        throw new ArgumentNullException("destination");

    foreach (var kv in source)
    {
        PropertyInfo p;
        if (PropertyMap.TryGetValue(kv.Key.ToLower(), out p))
        {
            p.SetValue(destination, kv.Value, null);
        }
        else
        {
            FieldInfo f;
            if (FieldMap.TryGetValue(kv.Key.ToLower(), out f))
            {
                f.SetValue(destination, kv.Value);
            }
        }
    }
}

1 - It does not recursively map the inner objects of the ExpandoObject as supposed.

Seems to work for your InnerClass at least.

Class c = new Class();
dynamic o = new ExpandoObject();
o.Name = "Carl";
o.Level = 7;
o.Inner = new InnerClass
{
    Name = "Inner Carl",
    Level = 10
};

o.Property = "my Property value"; // dont forget to set this

Mapper<Class>.Map(o, c);

EDIT: based on your comments, I've create two overloaded methods MergeProperty. You can write similarly overloaded methods for fields.

public static void MergeProperty(PropertyInfo pi, ExpandoObject source, object target)
{
    Type propType = pi.PropertyType;

    // dont recurse for value type, Nullable<T> and strings
    if (propType.IsValueType || propType == typeof(string))
    {
        var sourceVal = source.First(kvp => kvp.Key == pi.Name).Value;
        if(sourceVal != null)
            pi.SetValue(target, sourceVal, null);
    }
    else // recursively map inner class properties
    {
        var props = propType.GetProperties(BindingFlags.Public |
                                                  BindingFlags.NonPublic |
                                                  BindingFlags.Instance);

        foreach (var p in props)
        {
            var sourcePropValue = source.First(kvp => kvp.Key == pi.Name).Value;
            var targetPropValue = pi.GetValue(target, null);

            if (sourcePropValue != null)
            {
                if (targetPropValue == null) // replace
                {
                    pi.SetValue(target, source.First(kvp => kvp.Key == pi.Name).Value, null);
                }
                else
                {
                    MergeProperty(p, sourcePropValue, targetPropValue);
                }
            }
        }

    }
}

public static void MergeProperty(PropertyInfo pi, object source, object target)
{
    Type propType = pi.PropertyType;
    PropertyInfo sourcePi = source.GetType().GetProperty(pi.Name);

    // dont recurse for value type, Nullable<T> and strings
    if (propType.IsValueType || propType == typeof(string)) 
    {
        var sourceVal = sourcePi.GetValue(source, null);
        if(sourceVal != null)
            pi.SetValue(target, sourceVal, null);
    }
    else // recursively map inner class properties
    {
        var props = propType.GetProperties(BindingFlags.Public |
                                                  BindingFlags.NonPublic |
                                                  BindingFlags.Instance);

        foreach (var p in props)
        {
            var sourcePropValue = sourcePi.GetValue(source, null);
            var targetPropValue = pi.GetValue(target, null);

            if (sourcePropValue != null)
            {
                if (targetPropValue == null) // replace
                {
                    pi.SetValue(target, sourcePi.GetValue(source, null), null);
                }
                else
                {
                    MergeProperty(p, sourcePropValue, targetPropValue);
                }
            }
        }

    }
}

You can use the methods this way:

public static void Map(ExpandoObject source, T destination)
{
    if (source == null)
        throw new ArgumentNullException("source");
    if (destination == null)
        throw new ArgumentNullException("destination");

    foreach (var kv in source)
    {
        PropertyInfo p;
        if (PropertyMap.TryGetValue(kv.Key.ToLower(), out p))
        {
            MergeProperty(p, source, destination);
        }
        else
        {
            // do similar merge for fields
        }
    }
}
Saurischian answered 22/10, 2013 at 23:27 Comment(9)
Well nice detailed answer, However, it will work with the InnerClass but it will replace it not Map it... as i want to update (map) the values as it has done with the rest of non-inner properties [in another words (ignore null values from the source and do not replace the existing ones with null)]. Also consider arranging the answer 1,2,3 instead of 3,2,1 :DVerify
Can you elaborate it will replace it not Map it? Do you want to clone? How are non inner values mapped and not replaced? I did not understand.Saurischian
mmm, what i want to tell is. The job of that mapper is to update the destination object fields/properties, and what i mean with update is if the mapper found a null field/property in the source, it will not replace the destination corresponding field/property with null but just keep it, else it will update it. Second what i lack here too is that i want the mapper to look if the field/property contains more inner properties, it do the same process to it (this is what i mean with Recursively Mapping)Verify
And i really appreciate your will to help and hope you have some patience with me because this problem is very critical to me and really wanna get to an end with it.Verify
I get what you want.. will need some time.. will get back if I figure something useful for you.Saurischian
So please don't forget about me :D, i am waitingVerify
It doesn't map Arrays, any ideas ?Verify
Another thing, is what if a property contains inner fields, or a field contains inner properties how do we handle this.Verify
Arrays you can handle in non-recursive case. I think you will want to replace arrays and not map them. If you have written two overloads MergeFields as I suggested, you should be able to call them from MergeProperty and vice-versa. Overall, I hope you get the direction.Saurischian

© 2022 - 2024 — McMap. All rights reserved.