C # глубокое / вложенное / рекурсивное слияние динамических / расширяемых объектов - PullRequest
8 голосов
/ 27 января 2012

Мне нужно «объединить» 2 динамических объекта в C #. Все, что я нашел на stackexchange, касалось только нерекурсивного слияния. Но я ищу что-то, что делает рекурсивное или глубокое слияние, очень похожее на функцию jQuery $.extend(obj1, obj2).

При столкновении двух участников должны применяться следующие правила:

  • Если типы не совпадают, должно быть сгенерировано исключение и слияние отменено. Исключение: obj2 Значение может быть нулевым, в этом случае используется значение и тип obj1.
  • Для тривиальных типов (типы значений + строка) значения obj1 всегда предпочтительнее
  • Для нетривиальных типов применяются следующие правила:
    • IEnumerable & IEnumberables<T> просто объединены (может быть .Concat()?)
    • IDictionary и IDictionary<TKey,TValue> объединены; Ключи obj1 имеют приоритет при столкновении
    • Expando & Expando[] типы должны быть рекурсивно объединены, тогда как 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" }
    }
};

Есть идеи, как это сделать?

Ответы [ 2 ]

3 голосов
/ 18 апреля 2012

Да, это немного длинновато, но посмотрите. это реализация с использованием Reflection.Emit.

Для меня открыт вопрос, как реализовать переопределение ToString (), чтобы вы могли сравнивать строки. Эти значения приходят из файла конфигурации или что-то? Я думаю, если бы они были в формате JSON, вы могли бы сделать хуже, чем использовать JsonSerializer. Зависит от того, что вы хотите.

Вы можете использовать объект Expando, чтобы избавиться от глупостей 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;

Я не вижу способа обойти грязный код для анализа исходного дерева объектов, хотя и не сразу. Я хотел бы услышать некоторые другие мнения по этому поводу. 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;
    }
}
0 голосов
/ 24 октября 2018

Это работает для меня, но я уверен, что ему можно уделить немного любви и внимания и выглядеть лучше.Он не включает проверку типов, но добавить ее было бы довольно просто.Поэтому, хотя это не идеальный ответ, я надеюсь, что он поможет вам приблизиться к решению.

Последующие вызовы DynamicIntoExpando (...) будут продолжать добавлять и перезаписывать новые и существующие значения в существующей структуре источника.Вы можете звонить столько раз, сколько вам нужно.Функция 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<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;
        }
    }
}
...