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; }
}
Roman Ratskey
  • 5,101
  • 8
  • 44
  • 67

1 Answers1

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
        }
    }
}
YK1
  • 7,327
  • 1
  • 21
  • 28
  • 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 :D – Roman Ratskey Oct 22 '13 at 23:32
  • 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. – YK1 Oct 22 '13 at 23:39
  • 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) – Roman Ratskey Oct 22 '13 at 23:44
  • 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. – Roman Ratskey Oct 22 '13 at 23:45
  • 1
    I get what you want.. will need some time.. will get back if I figure something useful for you. – YK1 Oct 23 '13 at 00:24
  • So please don't forget about me :D, i am waiting – Roman Ratskey Oct 23 '13 at 00:42
  • It doesn't map Arrays, any ideas ? – Roman Ratskey Oct 24 '13 at 04:32
  • Another thing, is what if a property contains inner fields, or a field contains inner properties how do we handle this. – Roman Ratskey Oct 24 '13 at 04:48
  • 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. – YK1 Oct 24 '13 at 08:59