From f5503cdd3aaa4f267f47b2084e80a78d95e9bbaa Mon Sep 17 00:00:00 2001 From: Yves Goergen Date: Thu, 22 Aug 2019 14:37:41 +0200 Subject: [PATCH] #4: Added object/dictionary conversion and duck typing; Refactored into multiple files --- DeepConvert.Tests/DeepConvertTests.cs | 169 ++++++++++ DeepConvert/ConvertTypeInfo.cs | 362 ++++++++++++++++++++ DeepConvert/DeepConvert.ToX.cs | 329 ++++++++++++++++++ DeepConvert/DeepConvert.cs | 460 ++++---------------------- DeepConvert/DeepConvert.csproj | 18 +- DeepConvert/DeepConvertSettings.cs | 86 +++++ 6 files changed, 1021 insertions(+), 403 deletions(-) create mode 100644 DeepConvert/ConvertTypeInfo.cs create mode 100644 DeepConvert/DeepConvert.ToX.cs create mode 100644 DeepConvert/DeepConvertSettings.cs diff --git a/DeepConvert.Tests/DeepConvertTests.cs b/DeepConvert.Tests/DeepConvertTests.cs index 014953e..1c62584 100644 --- a/DeepConvert.Tests/DeepConvertTests.cs +++ b/DeepConvert.Tests/DeepConvertTests.cs @@ -354,5 +354,174 @@ public void ChangeType_StringToBytes() Assert.AreEqual(0xac, bytesArr[8]); // € Assert.AreEqual(0x20, bytesArr[9]); } + + public class DataClass + { + public bool Flag { get; set; } + public int Number { get; set; } + public string Text { get; set; } + public int[] Numbers { get; set; } + public List Floats { get; set; } + public SubClass Sub { get; set; } + } + + public class DataClass2 + { + public int flag { get; set; } + public long number { get; set; } + public string Text { get; set; } + public double[] Floats { get; set; } + public bool Add { get; set; } = true; + public SubClass2 Sub { get; set; } + } + + public class SubClass + { + public bool IsInitialized { get; set; } + public Dictionary Dict { get; set; } + } + + public class SubClass2 + { + public int IsInitialized { get; set; } + public SubDict2 Dict { get; set; } + + public class SubDict2 + { + public short A { get; set; } + public short B { get; set; } + public short C { get; set; } + } + } + + [TestMethod] + public void ConvertToObject() + { + var dict = new Dictionary + { + ["flag"] = true, + ["number"] = 42, + ["Text"] = "Hello", + ["Numbers"] = new[] { 1.0, 2.0, 3.6 }, + ["Floats"] = new[] { "3.14", "2.71" }, + ["Sub"] = new Dictionary + { + ["IsInitialized"] = true, + ["Dict"] = new Dictionary + { + ["A"] = 1, + ["B"] = 2, + ["C"] = 3 + } + } + }; + //var data = DeepConvert.ConvertToObject(dict, new DeepConvertSettings { Provider = CultureInfo.InvariantCulture }); + var data = DeepConvert.ChangeType(dict, new DeepConvertSettings { Provider = CultureInfo.InvariantCulture }); + + Assert.AreEqual(true, data.Flag); + Assert.AreEqual(42, data.Number); + Assert.AreEqual("Hello", data.Text); + + Assert.AreEqual(3, data.Numbers.Length); + Assert.AreEqual(1, data.Numbers[0]); + Assert.AreEqual(2, data.Numbers[1]); + Assert.AreEqual(4, data.Numbers[2]); + + Assert.AreEqual(2, data.Floats.Count); + Assert.AreEqual(3.14f, data.Floats[0]); + Assert.AreEqual(2.71f, data.Floats[1]); + + Assert.AreEqual(true, data.Sub.IsInitialized); + + Assert.AreEqual(3, data.Sub.Dict.Count); + Assert.AreEqual("1", data.Sub.Dict["A"]); + Assert.AreEqual("2", data.Sub.Dict["B"]); + Assert.AreEqual("3", data.Sub.Dict["C"]); + } + + [TestMethod] + public void ConvertToDictionary() + { + var data = new DataClass + { + Flag = true, + Number = 42, + Text = "Hello", + Numbers = new[] { 1, 2, 4 }, + Floats = new List { 3.14f, 2.71f }, + Sub = new SubClass + { + IsInitialized = true, + Dict = new Dictionary + { + ["A"] = "1", + ["B"] = "2", + ["C"] = "3" + } + } + }; + //var dict = DeepConvert.ConvertToDictionary(data); + var dict = DeepConvert.ChangeType>(data); + + Assert.AreEqual(true, dict["Flag"]); + Assert.AreEqual(42, dict["Number"]); + Assert.AreEqual("Hello", dict["Text"]); + + Assert.AreEqual(3, ((int[])dict["Numbers"]).Length); + Assert.AreEqual(1, ((int[])dict["Numbers"])[0]); + Assert.AreEqual(2, ((int[])dict["Numbers"])[1]); + Assert.AreEqual(4, ((int[])dict["Numbers"])[2]); + + Assert.AreEqual(2, ((List)dict["Floats"]).Count); + Assert.AreEqual(3.14f, ((List)dict["Floats"])[0]); + Assert.AreEqual(2.71f, ((List)dict["Floats"])[1]); + + var sub = (SubClass)dict["Sub"]; + Assert.AreEqual(true, sub.IsInitialized); + + Assert.AreEqual(3, sub.Dict.Count); + Assert.AreEqual("1", sub.Dict["A"]); + Assert.AreEqual("2", sub.Dict["B"]); + Assert.AreEqual("3", sub.Dict["C"]); + } + + [TestMethod] + public void ConvertBetweenObjects() + { + var data = new DataClass + { + Flag = true, + Number = 42, + Text = "Hello", + Numbers = new[] { 1, 2, 4 }, + Floats = new List { 3.14f, 2.71f }, + Sub = new SubClass + { + IsInitialized = true, + Dict = new Dictionary + { + ["A"] = "1", + ["B"] = "2", + ["C"] = "3" + } + } + }; + var data2 = DeepConvert.ChangeType(data); + + Assert.AreEqual(1, data2.flag); + Assert.AreEqual(42, data2.number); + Assert.AreEqual("Hello", data2.Text); + Assert.AreEqual(true, data2.Add); + + Assert.AreEqual(2, data2.Floats.Length); + Assert.AreEqual(3.14f, data2.Floats[0]); + Assert.AreEqual(2.71f, data2.Floats[1]); + + Assert.AreEqual(1, data2.Sub.IsInitialized); + + Assert.AreEqual(1, data2.Sub.Dict.A); + Assert.AreEqual(2, data2.Sub.Dict.B); + Assert.AreEqual(3, data2.Sub.Dict.C); + } } } diff --git a/DeepConvert/ConvertTypeInfo.cs b/DeepConvert/ConvertTypeInfo.cs new file mode 100644 index 0000000..d94d421 --- /dev/null +++ b/DeepConvert/ConvertTypeInfo.cs @@ -0,0 +1,362 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using System.Reflection.Emit; + +// Expression will always cause a System.NullReferenceException because the default value of +// 'generic type' is null +#pragma warning disable 1720 + +namespace Unclassified.Util +{ + internal class ConvertTypeInfo + { + #region Static members + + private static AssemblyBuilder assemblyBuilder; + private static ModuleBuilder moduleBuilder; + + static ConvertTypeInfo() + { + assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( + new AssemblyName() { Name = Guid.NewGuid().ToString() }, + AssemblyBuilderAccess.Run); + moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyBuilder.GetName().Name); + } + + #endregion Static members + + #region Private data + + private readonly string[] names; + private readonly Func getter; + private readonly Action setter; + + #endregion Private data + + #region Constructors + + public ConvertTypeInfo(Type type) + { + Type = type; + + var namesList = new List(); + var typeBuilder = moduleBuilder.DefineType(type.Name + "Converter", TypeAttributes.Class | TypeAttributes.Public); + + var gettableProperties = new List(); + var settableProperties = new List(); + + foreach (PropertyInfo propertyInfo in GetAllProperties(type)) + { + bool added = false; + if (propertyInfo.GetGetMethod() != null) + { + gettableProperties.Add(propertyInfo); + added = true; + } + if (propertyInfo.GetSetMethod() != null) + { + settableProperties.Add(propertyInfo); + added = true; + } + if (added) + { + namesList.Add(propertyInfo.Name); + } + } + + BuildGetMethod(typeBuilder, gettableProperties); + BuildSetMethod(typeBuilder, settableProperties); + + names = namesList.ToArray(); + + var converterType = typeBuilder.CreateTypeInfo(); + getter = (Func)converterType.GetMethod("Get") + .CreateDelegate(typeof(Func)); + setter = (Action)converterType.GetMethod("Set") + .CreateDelegate(typeof(Action)); + } + + #endregion Constructors + + #region Public members + + public Type Type { get; } + + public object CreateInstance() => Activator.CreateInstance(Type); + + public string[] GetNames() => names; + + public object GetValue(object instance, string name) + { + return getter(instance, name); + } + + public void SetValue(object instance, string name, object value, DeepConvertSettings settings) + { + setter(instance, name, value, settings); + } + + #endregion Public members + + #region Private methods + + // From Unclassified.Util.TypeExtensions.GetAllProperties method + private static PropertyInfo[] GetAllProperties(Type type) + { + var properties = new List(); + do + { + // Only consider properties declared in this type directly + foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly)) + { + properties.Add(property); + } + // Also include properties from the base type (recursive) + type = type.BaseType; + } + while (type != null); + return properties.ToArray(); + } + + private void BuildGetMethod(TypeBuilder typeBuilder, List properties) + { + MethodBuilder methodBuilder = typeBuilder.DefineMethod( + "Get", + MethodAttributes.Public | MethodAttributes.Static, + typeof(object), + new[] + { + typeof(object), // instance + typeof(string) // property name + }); + + ILGenerator ilGen = methodBuilder.GetILGenerator(); + ilGen.DeclareLocal(Type); // typedInstance (local index 0) + ilGen.DeclareLocal(typeof(string)); // lowerName (local index 1) + + // var typedInstance = (MyType)instance; + ilGen.Emit(OpCodes.Ldarg_0); + ilGen.Emit(OpCodes.Castclass, Type); + ilGen.Emit(OpCodes.Stloc_0); + + // string lowerName = name.ToLowerInvariant(); + ilGen.Emit(OpCodes.Ldarg_1); + ilGen.EmitCall(OpCodes.Callvirt, MethodOf(() => default(string).ToLowerInvariant()), null); + ilGen.Emit(OpCodes.Stloc_1); + + var labels = new List