Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# deep/nested/recursive merge of dynamic/expando objects

I need to "merge" 2 dynamic objects in C#. All that I've found on stackexchange covered only non-recursive merging. But I am looking to something that does recursive or deep merging, very much the same like jQuery's $.extend(obj1, obj2) function.

Upon collision of two members, the following rules should apply:

  • If the types mismatch, an exception must be thrown and merge is aborted. Exception: obj2 Value maybe null, in this case the value & type of obj1 is used.
  • For trivial types (value types + string) obj1 values are always prefered
  • For non-trivial types, the following rules are applied:
    • IEnumerable & IEnumberables<T> are simply merged (maybe .Concat() ? )
    • IDictionary & IDictionary<TKey,TValue> are merged; obj1 keys have precedence upon collision
    • Expando & Expando[] types must be merged recursively, whereas Expando[] will always have same-type elements only
    • One can assume there are no Expando objects within Collections (IEnumerabe & IDictionary)
  • All other types can be discarded and need not be present in the resulting dynamic object

Here is an example of a possible merge:

dynamic DefaultConfig = new {
    BlacklistedDomains = new string[] { "domain1.com" },
    ExternalConfigFile = "blacklist.txt",
    UseSockets = new[] {
        new { IP = "127.0.0.1", Port = "80"},
        new { IP = "127.0.0.2", Port = "8080" }
    }
};

dynamic UserSpecifiedConfig = new {
    BlacklistedDomain = new string[] { "example1.com" },
    ExternalConfigFile = "C:\\my_blacklist.txt"
};

var result = Merge (UserSpecifiedConfig, DefaultConfig);
// result should now be equal to:
var result_equal = new {
    BlacklistedDomains = new string[] { "domain1.com", "example1.com" },
    ExternalConfigFile = "C:\\my_blacklist.txt",
    UseSockets = new[] {
        new { IP = "127.0.0.1", Port = "80"},
        new { IP = "127.0.0.2", Port = "8080" }
    }
};

Any ideas how to do this?

like image 904
Dynalon Avatar asked Jan 27 '12 17:01

Dynalon


1 Answers

Right, this is a bit longwinded but have a look. it's an implementation using Reflection.Emit.

Open issue for me is how to implement a ToString() override so that you can do a string comparison. Are these values coming from a config file or something? If they are in JSON Format you could do worse than use a JsonSerializer, I think. Depends on what you want.

You could use the Expando Object to get rid of the Reflection.Emit nonsense as well, at the bottom of the loop:

var result = new ExpandoObject();
var resultDict = result as IDictionary<string, object>;
foreach (string key in resVals.Keys)
{
    resultDict.Add(key, resVals[key]);
}
return result;

I can't see a way around the messy code for parsing the original object tree though, not immediately. I'd like to hear some other opinions on this. The DLR is relatively new ground for me.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            dynamic DefaultConfig = new
            {
                BlacklistedDomains = new string[] { "domain1.com" },
                ExternalConfigFile = "blacklist.txt",
                UseSockets = new[] { 
                    new { IP = "127.0.0.1", Port = "80" }, 
                    new { IP = "127.0.0.2", Port = "8080" } 
                }
            };

            dynamic UserSpecifiedConfig = new
            {
                BlacklistedDomains = new string[] { "example1.com" },
                ExternalConfigFile = "C:\\my_blacklist.txt"
            };

            var result = Merge(UserSpecifiedConfig, DefaultConfig);

            // result should now be equal to: 

            var result_equal = new
            {
                BlacklistedDomains = new string[] { "domain1.com", "example1.com" },
                ExternalConfigFile = "C:\\my_blacklist.txt",
                UseSockets = new[] {         
                    new { IP = "127.0.0.1", Port = "80"},         
                    new { IP = "127.0.0.2", Port = "8080" }     
                }
            };
            Debug.Assert(result.Equals(result_equal));
        }

        /// <summary>
        /// Merge the properties of two dynamic objects, taking the LHS as primary
        /// </summary>
        /// <param name="lhs"></param>
        /// <param name="rhs"></param>
        /// <returns></returns>
        static dynamic Merge(dynamic lhs, dynamic rhs)
        {
            // get the anonymous type definitions
            Type lhsType = ((Type)((dynamic)lhs).GetType());
            Type rhsType = ((Type)((dynamic)rhs).GetType());

            object result = new { };
            var resProps = new Dictionary<string, PropertyInfo>();
            var resVals = new Dictionary<string, object>();

            var lProps = lhsType.GetProperties().ToDictionary<PropertyInfo, string>(prop => prop.Name);
            var rProps = rhsType.GetProperties().ToDictionary<PropertyInfo, string>(prop => prop.Name); 


            foreach (string leftPropKey in lProps.Keys)
            {
                var lPropInfo = lProps[leftPropKey];
                resProps.Add(leftPropKey, lPropInfo);
                var lhsVal = Convert.ChangeType(lPropInfo.GetValue(lhs, null), lPropInfo.PropertyType);
                if (rProps.ContainsKey(leftPropKey))
                {
                    PropertyInfo rPropInfo;
                    rPropInfo = rProps[leftPropKey];
                    var rhsVal = Convert.ChangeType(rPropInfo.GetValue(rhs, null), rPropInfo.PropertyType);
                    object setVal = null;

                    if (lPropInfo.PropertyType.IsAnonymousType())
                    {
                        setVal = Merge(lhsVal, rhsVal);
                    }
                    else if (lPropInfo.PropertyType.IsArray)
                    {
                        var bound = ((Array) lhsVal).Length + ((Array) rhsVal).Length;
                        var cons = lPropInfo.PropertyType.GetConstructor(new Type[] { typeof(int) });
                        dynamic newArray = cons.Invoke(new object[] { bound });
                        //newArray = ((Array)lhsVal).Clone();
                        int i=0;
                        while (i < ((Array)lhsVal).Length)
                        {
                            newArray[i] = lhsVal[i];
                            i++;
                        }
                        while (i < bound)
                        {
                            newArray[i] = rhsVal[i - ((Array)lhsVal).Length];
                            i++;
                        }
                        setVal = newArray;
                    }
                    else
                    {
                        setVal = lhsVal == null ? rhsVal : lhsVal;
                    }
                    resVals.Add(leftPropKey, setVal);
                }
                else 
                {
                    resVals.Add(leftPropKey, lhsVal);
                }
            }
            foreach (string rightPropKey in rProps.Keys)
            {
                if (lProps.ContainsKey(rightPropKey) == false)
                {
                    PropertyInfo rPropInfo;
                    rPropInfo = rProps[rightPropKey];
                    var rhsVal = rPropInfo.GetValue(rhs, null);
                    resProps.Add(rightPropKey, rPropInfo);
                    resVals.Add(rightPropKey, rhsVal);
                }
            }

            Type resType = TypeExtensions.ToType(result.GetType(), resProps);

            result = Activator.CreateInstance(resType);

            foreach (string key in resVals.Keys)
            {
                var resInfo = resType.GetProperty(key);
                resInfo.SetValue(result, resVals[key], null);
            }
            return result;
        }
    }
}

public static class TypeExtensions
{
    public static Type ToType(Type type, Dictionary<string, PropertyInfo> properties)
    {
        AppDomain myDomain = Thread.GetDomain();
        Assembly asm = type.Assembly;
        AssemblyBuilder assemblyBuilder = 
            myDomain.DefineDynamicAssembly(
            asm.GetName(), 
            AssemblyBuilderAccess.Run
        );
        ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(type.Module.Name);
        TypeBuilder typeBuilder = moduleBuilder.DefineType(type.Name,TypeAttributes.Public);

        foreach (string key in properties.Keys)
        {
            string propertyName = key;
            Type propertyType = properties[key].PropertyType;

            FieldBuilder fieldBuilder = typeBuilder.DefineField(
                "_" + propertyName,
                propertyType,
                FieldAttributes.Private
            );

            PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(
                propertyName,
                PropertyAttributes.HasDefault,
                propertyType,
                new Type[] { }
            );
            // First, we'll define the behavior of the "get" acessor for the property as a method.
            MethodBuilder getMethodBuilder = typeBuilder.DefineMethod(
                "Get" + propertyName,
                MethodAttributes.Public,
                propertyType,
                new Type[] { }
            );

            ILGenerator getMethodIL = getMethodBuilder.GetILGenerator();

            getMethodIL.Emit(OpCodes.Ldarg_0);
            getMethodIL.Emit(OpCodes.Ldfld, fieldBuilder);
            getMethodIL.Emit(OpCodes.Ret);

            // Now, we'll define the behavior of the "set" accessor for the property.
            MethodBuilder setMethodBuilder = typeBuilder.DefineMethod(
                "Set" + propertyName,
                MethodAttributes.Public,
                null,
                new Type[] { propertyType }
            );

            ILGenerator custNameSetIL = setMethodBuilder.GetILGenerator();

            custNameSetIL.Emit(OpCodes.Ldarg_0);
            custNameSetIL.Emit(OpCodes.Ldarg_1);
            custNameSetIL.Emit(OpCodes.Stfld, fieldBuilder);
            custNameSetIL.Emit(OpCodes.Ret);

            // Last, we must map the two methods created above to our PropertyBuilder to 
            // their corresponding behaviors, "get" and "set" respectively. 
            propertyBuilder.SetGetMethod(getMethodBuilder);
            propertyBuilder.SetSetMethod(setMethodBuilder);
        }

        //MethodBuilder toStringMethodBuilder = typeBuilder.DefineMethod(
        //    "ToString",
        //    MethodAttributes.Public,
        //    typeof(string),
        //    new Type[] { }
        //);

        return typeBuilder.CreateType();
    }
    public static Boolean IsAnonymousType(this Type type)
    {
        Boolean hasCompilerGeneratedAttribute = type.GetCustomAttributes(
            typeof(CompilerGeneratedAttribute), false).Count() > 0;
        Boolean nameContainsAnonymousType =
            type.FullName.Contains("AnonymousType");
        Boolean isAnonymousType = hasCompilerGeneratedAttribute && nameContainsAnonymousType;
        return isAnonymousType;
    }
}
like image 187
Mel Padden Avatar answered Oct 05 '22 23:10

Mel Padden