简体   繁体   English

动态/扩展对象的C#深度/嵌套/递归合并

[英]C# deep/nested/recursive merge of dynamic/expando objects

I need to "merge" 2 dynamic objects in C#. 我需要在C#中“合并” 2个动态对象。 All that I've found on stackexchange covered only non-recursive merging. 我在stackexchange上发现的所有内容仅涉及非递归合并。 But I am looking to something that does recursive or deep merging, very much the same like jQuery's $.extend(obj1, obj2) function. 但是我正在寻找可以进行递归或深度合并的东西,就像jQuery的$.extend(obj1, obj2)函数一样。

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. 例外:obj2值可能为null,在这种情况下,将使用obj1的值和类型。
  • For trivial types (value types + string) obj1 values are always prefered 对于琐碎的类型(值类型+字符串),始终首选obj1值
  • For non-trivial types, the following rules are applied: 对于非平凡类型,将应用以下规则:
    • IEnumerable & IEnumberables<T> are simply merged (maybe .Concat() ? ) IEnumerableIEnumberables<T>被简单地合并(也许是.Concat() ?)
    • IDictionary & IDictionary<TKey,TValue> are merged; IDictionaryIDictionary<TKey,TValue>被合并; obj1 keys have precedence upon collision obj1键在冲突时优先
    • Expando & Expando[] types must be merged recursively, whereas Expando[] will always have same-type elements only ExpandoExpando[]类型必须递归合并,而Expando []将始终仅具有相同类型的元素
    • One can assume there are no Expando objects within Collections (IEnumerabe & IDictionary) 可以假定Collections中没有Expando对象(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? 任何想法如何做到这一点?

Right, this is a bit longwinded but have a look. 是的,这有些大碍,但请看一下。 it's an implementation using Reflection.Emit. 这是使用Reflection.Emit的实现。

Open issue for me is how to implement a ToString() override so that you can do a string comparison. 对我来说,一个开放的问题是如何实现ToString()覆盖,以便您可以进行字符串比较。 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. 我认为,如果它们采用JSON格式,则可能比使用JsonSerializer还要糟糕。 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: 您可以使用Expando Object摆脱Reflection.Emit废话,在循环的底部:

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. DLR对我来说是一个相对较新的领域。

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;
    }
}

This works for me, but I'm sure it could be given some love and attention and look better. 这对我有用,但是我敢肯定它会得到一些爱和关注,并且看起来会更好。 It does not include your type-check but that would be rather trivial to add. 它不包括您的类型检查,但是添加起来很简单。 So while this is not a perfect answer, I hope it can get you closer to a solution. 因此,尽管这不是一个完美的答案,但我希望它能使您更接近解决方案。

Subsequent calls to DynamicIntoExpando(...) will keep appending and overwriting new and existing values to the existing Source structure. 随后对DynamicIntoExpando(...)的调用将继续向现有Source结构附加和覆盖新值和现有值。 You can call it as many times as you need to. 您可以根据需要多次调用它。 The function MergeDynamic() illustrates how two dynamics are merged into one ExpandoObject. 函数MergeDynamic()说明了两个动态如何合并到一个ExpandoObject中。

The code basically iterates over the dynamic value, checks the type, and merge appropriately and recursively to any depth. 该代码基本上遍历动态值,检查类型,并适当地递归合并到任何深度。

I wrapped it in a helper class for my own purposes. 我出于自己的目的将其包装在帮助程序类中。

using System.Dynamic; // For ExpandoObject
...

public static class DynamicHelper
{

    // We expect inputs to be of type IDictionary
    public static ExpandoObject MergeDynamic(dynamic Source, dynamic Additional)
    {
        ExpandoObject Result = new ExpandoObject();

        // First copy 'source' to Result
        DynamicIntoExpando(Result, Source);

        // Then copy additional fields, boldy overwriting the source as needed
        DynamicIntoExpando(Result, Additional);

        // Done
        return Result;
    }

    public static void DynamicIntoExpando(ExpandoObject Result, dynamic Source, string Key = null)
    {
        // Cast it for ease of use.
        var R = Result as IDictionary<string, dynamic>;

        if (Source is IDictionary<string, dynamic>)
        {
            var S = Source as IDictionary<string, dynamic>;

            ExpandoObject NewDict = new ExpandoObject();
            if (Key == null)
            {
                NewDict = Result;
            }
            else if (R.ContainsKey(Key))
            {
                // Already exists, overwrite
                NewDict = R[Key];
            }
            var ND = NewDict as IDictionary<string, dynamic>;

            foreach (string key in S.Keys)
            {
                ExpandoObject NewDictEntry = new ExpandoObject();
                var NDE = NewDictEntry as IDictionary<string, dynamic>;
                if (ND.ContainsKey(key))
                {
                    NDE[key] = ND[key];
                }
                else if (R.ContainsKey(key))
                {
                    NDE[key] = R[key];
                }

                DynamicIntoExpando(NewDictEntry, S[key], key);
                if(!R.ContainsKey(key)) {
                    ND[key] = ((IDictionary<string, dynamic>)NewDictEntry)[key];
                }
            }
            if (Key == null)
            {
                R = NewDict;
            }
            else if (!R.ContainsKey(Key))
            {
                R.Add(Key, NewDict);
            }
        }
        else if (Source is IList<dynamic>)
        {
            var S = Source as IList<dynamic>;
            List<ExpandoObject> NewList = new List<ExpandoObject>();
            if (Key != null && R.ContainsKey(Key))
            {
                // Already exists, overwrite
                NewList = (List<ExpandoObject>)R[Key];
            }
            foreach (dynamic D in S)
            {
                ExpandoObject ListEntry = new ExpandoObject();
                DynamicIntoExpando(ListEntry, D);
                //  in this case we have to compare the ListEntry to existing entries and on

                NewList.Add(ListEntry);
            }
            if (Key != null && !R.ContainsKey(Key))
            {
                R[Key] = NewList.Distinct().ToList(); 
            }
        }
        else
        {
            R[Key] = Source;
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM