C#深度/嵌套/递归合并dynamic / expando对象

我需要在C#中“合并”2个动态对象。 我在stackexchange上发现的所有内容都只包含了非递归合并。 但我正在寻找一些递归或深度合并的东西,就像jQuery的$.extend(obj1, obj2)函数一样。

两名成员发生碰撞后,应遵守以下规则:

  • 如果类型不匹配,则必须抛出exception并中止合并。 例外:obj2值可能为null,在这种情况下使用obj1的值和类型。
  • 对于普通类型(值类型+字符串),总是首选obj1值
  • 对于非平凡类型,应用以下规则:
    • IEnumerableIEnumberables简单合并(也许.Concat() ?)
    • IDictionaryIDictionary合并; obj1键在碰撞时具有优先权
    • 必须以递归方式合并ExpandoExpando[]类型,而Expando []将始终只具有相同类型的元素
    • 可以假设集合中没有Expando对象(IEnumerabe&IDictionary)
  • 可以丢弃所有其他类型,并且不需要在生成的动态对象中存在

以下是可能合并的示例:

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

任何想法如何做到这一点?

对,这有点长,但看看。 它是使用Reflection.Emit的实现。

我公开的问题是如何实现ToString()覆盖,以便您可以进行字符串比较。 这些值是来自配置文件还是其他什么? 如果它们是JSON格式,你可能比使用JsonSerializer更糟糕,我想。 取决于你想要什么。

您可以使用Expando对象来摆脱循环底部的Reflection.Emit废话:

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

我无法看到解决原始对象树的杂乱代码,但不能立即解决。 我想听听其他一些意见。 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)); } ///  /// Merge the properties of two dynamic objects, taking the LHS as primary ///  ///  ///  ///  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(); var resVals = new Dictionary(); var lProps = lhsType.GetProperties().ToDictionary(prop => prop.Name); var rProps = rhsType.GetProperties().ToDictionary(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 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; } } 

这适合我,但我相信它可以给予一些爱和关注,看起来更好。 它不包括你的类型检查,但添加起来相当简单。 虽然这不是一个完美的答案,但我希望它可以让你更接近解决方案。

对DynamicIntoExpando(…)的后续调用将继续追加并覆盖现有Source结构的新值和现有值。 您可以根据需要多次调用它。 MergeDynamic()函数说明了如何将两个动态合并到一个ExpandoObject中。

代码基本上迭代动态值,检查类型,并适当地和递归地合并到任何深度。

为了我自己的目的,我把它包装在一个帮助类中。

 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; if (Source is IDictionary) { var S = Source as IDictionary; 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; foreach (string key in S.Keys) { ExpandoObject NewDictEntry = new ExpandoObject(); var NDE = NewDictEntry as IDictionary; 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)NewDictEntry)[key]; } } if (Key == null) { R = NewDict; } else if (!R.ContainsKey(Key)) { R.Add(Key, NewDict); } } else if (Source is IList) { var S = Source as IList; List NewList = new List(); if (Key != null && R.ContainsKey(Key)) { // Already exists, overwrite NewList = (List)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; } } }