From 08db4fa097d39ba27268c13f274c66658d8a3a1b Mon Sep 17 00:00:00 2001 From: deipax Date: Tue, 5 Sep 2017 12:25:39 -0700 Subject: [PATCH 01/15] Add ArrayPrimitiveTypeExpressionFactory. When cloning Primitives all that needs to be done is copy the source array to the target array. Instead of calling the PrimitiveTypeExpressionFactory call for every element you can use the static Array.Copy method to assist. Depending upon the primitive type, the performance gain is different. In the tests I have, which are coded against 4.6.1, will have a performance gain of any where from about 98% to almost 225%. Byte Array, for whatever reason, has a performance gain of about 6800% ... I have no idea why byte arrays are so much different. Int Arrays go from about 2,386,634 ops per sec to 4,727,659 (98% increase) String arrays go from about 1,926,782 ops per sec to 5,730,086 (197% increase) TimeSpan arrays go from about 1,988,071 ops per sec to 4,650,697 (133% increase) DateTime arrays go from about 1,930,501 ops per sec to 4,210,105 (118% increase) Delegate arrays go from about 1,659,751 ops per sec to 5,404,864 (225% increase) Byte arrays go from about 58,083 ops per sec to 4,053,648 (6879% increase) I would like supply a unit test to easily display the difference in performance in the master project, but given the interface, it is not exactly straight forward. I generated the numbers above from running the changes suggested from a local release build in my current test setup. --- .../ArrayExpressionFactory.cs | 4 +- .../ArrayPrimitiveTypeExpressionFactory.cs | 41 +++++++++++++++++++ src/CloneExtensions/ExpressionFactory.cs | 18 +++++++- 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 src/CloneExtensions/ExpressionFactories/ArrayPrimitiveTypeExpressionFactory.cs diff --git a/src/CloneExtensions/ExpressionFactories/ArrayExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/ArrayExpressionFactory.cs index 0756d1a..4a1ea44 100644 --- a/src/CloneExtensions/ExpressionFactories/ArrayExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/ArrayExpressionFactory.cs @@ -9,7 +9,7 @@ class ArrayExpressionFactory : DeepShallowExpressionFactoryBase { private Type _itemType; private Expression _arrayLength; - private Expression _newArray; + protected Expression _newArray; public ArrayExpressionFactory(ParameterExpression source, Expression target, ParameterExpression flags, ParameterExpression initializers, ParameterExpression clonedObjects) : base(source, target, flags, initializers, clonedObjects) @@ -69,4 +69,4 @@ private static Type GetItemType() .First(); } } -} +} \ No newline at end of file diff --git a/src/CloneExtensions/ExpressionFactories/ArrayPrimitiveTypeExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/ArrayPrimitiveTypeExpressionFactory.cs new file mode 100644 index 0000000..22925c7 --- /dev/null +++ b/src/CloneExtensions/ExpressionFactories/ArrayPrimitiveTypeExpressionFactory.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace CloneExtensions.ExpressionFactories +{ + class ArrayPrimitiveTypeExpressionFactory : ArrayExpressionFactory + { + private static MethodInfo _copyTo = typeof(Array).GetRuntimeMethod( + "CopyTo", + new Type[2] { typeof(Array), typeof(int) }); + + public ArrayPrimitiveTypeExpressionFactory( + ParameterExpression source, + Expression target, + ParameterExpression flags, + ParameterExpression initializers, + ParameterExpression clonedObjects) + : base(source, target, flags, initializers, clonedObjects) + { + } + + protected override Expression GetCloneExpression(Func getItemCloneExpression) + { + var assign = Expression.Assign( + Target, + _newArray); + + var copy = Expression.Call( + Source, + _copyTo, + Target, + Expression.Constant(0)); + + return Expression.Block( + assign, + GetAddToClonedObjectsExpression(), + copy); + } + } +} \ No newline at end of file diff --git a/src/CloneExtensions/ExpressionFactory.cs b/src/CloneExtensions/ExpressionFactory.cs index ad7147a..9ac10d4 100644 --- a/src/CloneExtensions/ExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactory.cs @@ -1,6 +1,7 @@ using CloneExtensions.ExpressionFactories; using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; namespace CloneExtensions @@ -108,7 +109,20 @@ private static IExpressionFactory GetExpressionFactory(ParameterExpression so } else if (_type.IsArray) { - return new ArrayExpressionFactory(source, target, flags, initializers, clonedObjects); + var itemType = _type + .GetInterfaces() + .First(x => x.IsGenericType() && x.GetGenericTypeDefinition() == typeof(ICollection<>)) + .GetGenericArguments() + .First(); + + if (_type.IsPrimitiveOrKnownImmutable() || typeof(Delegate).IsAssignableFrom(_type)) + { + return new ArrayPrimitiveTypeExpressionFactory(source, target, flags, initializers, clonedObjects); + } + else + { + return new ArrayExpressionFactory(source, target, flags, initializers, clonedObjects); + } } else if (_type.IsGenericType() && (_type.GetGenericTypeDefinition() == typeof(Tuple<>) @@ -130,4 +144,4 @@ private static IExpressionFactory GetExpressionFactory(ParameterExpression so return new ComplexTypeExpressionFactory(source, target, flags, initializers, clonedObjects); } } -} +} \ No newline at end of file From 867690bc73bf7f0052c83a479b0c5ef49cd37434 Mon Sep 17 00:00:00 2001 From: deipax Date: Tue, 5 Sep 2017 12:33:16 -0700 Subject: [PATCH 02/15] Small fix. --- src/CloneExtensions/ExpressionFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CloneExtensions/ExpressionFactory.cs b/src/CloneExtensions/ExpressionFactory.cs index 9ac10d4..ea14d58 100644 --- a/src/CloneExtensions/ExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactory.cs @@ -115,7 +115,7 @@ private static IExpressionFactory GetExpressionFactory(ParameterExpression so .GetGenericArguments() .First(); - if (_type.IsPrimitiveOrKnownImmutable() || typeof(Delegate).IsAssignableFrom(_type)) + if (itemType.IsPrimitiveOrKnownImmutable() || typeof(Delegate).IsAssignableFrom(itemType)) { return new ArrayPrimitiveTypeExpressionFactory(source, target, flags, initializers, clonedObjects); } From d2035d97be1324c5abc0a2a4e3f5b40ef66298a6 Mon Sep 17 00:00:00 2001 From: deipax Date: Wed, 6 Sep 2017 12:23:41 -0700 Subject: [PATCH 03/15] Add support for polymorphism. In order to support polymorphism, we have to find the runtime type and run the appropriate CloneDelegate against the source object. I added the CloneIdDelegateCache to assist with this process. I needed to make a couple changes to the ComplexTypeExpression factory to add polymorphism support. In order to maintain backwards compatibility, if an initializer was supplied by the user, that should be used before the call to the new code. I added a new test class that asserts several scenarios, including the scenario outlined in issue #7. Also in the new unit test, I added some SpeedComparions tests (which I commented out for integration purposes) to help give an idea of what type of performance hit this change will cause. Let me know what you think. --- .../CloneItDelegateCacheTests.cs | 36 +++ .../ComplexTypeTests.cs | 24 +- .../Helpers/RandGen.cs | 91 +++++++ .../Helpers/TimingHelper.cs | 142 +++++++++++ .../PolymorphismSupportTests.cs | 238 ++++++++++++++++++ src/CloneExtensions/CloneItDelegateCache.cs | 88 +++++++ .../ComplexTypeExpressionFactory.cs | 53 +++- 7 files changed, 662 insertions(+), 10 deletions(-) create mode 100644 src/CloneExtensions.UnitTests/CloneItDelegateCacheTests.cs create mode 100644 src/CloneExtensions.UnitTests/Helpers/RandGen.cs create mode 100644 src/CloneExtensions.UnitTests/Helpers/TimingHelper.cs create mode 100644 src/CloneExtensions.UnitTests/PolymorphismSupportTests.cs create mode 100644 src/CloneExtensions/CloneItDelegateCache.cs diff --git a/src/CloneExtensions.UnitTests/CloneItDelegateCacheTests.cs b/src/CloneExtensions.UnitTests/CloneItDelegateCacheTests.cs new file mode 100644 index 0000000..a7bdbe6 --- /dev/null +++ b/src/CloneExtensions.UnitTests/CloneItDelegateCacheTests.cs @@ -0,0 +1,36 @@ +using CloneExtensions.UnitTests.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; + +namespace CloneExtensions.UnitTests +{ + [TestClass] + public class CloneItDelegateCacheTests + { + [TestMethod] + public void CloneItDelegateCacheTests_IReadOnlyList_Int() + { + IReadOnlyList source = new List() + { + RandGen.GenerateInt() + }; + + var cloneItDelegate = CloneItDelegateCache.Get(source); + + var flags = CloningFlags.Fields | CloningFlags.Properties | CloningFlags.CollectionItems; + var initializers = new Dictionary>(); + var clonedObjects = new Dictionary(); + + var target = cloneItDelegate(source, flags, initializers, clonedObjects); + + var targetAsList = target as List; + + Assert.IsNotNull(targetAsList); + Assert.AreNotSame(targetAsList, source); + Assert.AreEqual(targetAsList.Count, source.Count); + Assert.AreEqual(targetAsList[0], source[0]); + Assert.AreNotSame(targetAsList[0], source[0]); + } + } +} \ No newline at end of file diff --git a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs index c898dec..9358ad8 100644 --- a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs +++ b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs @@ -36,9 +36,9 @@ public void GetClone_SameTypepProperty_Cloned() [TestMethod] [ExpectedException(typeof(InvalidOperationException))] - public void GetClone_AbstractClassInitializerNotSpecified_InvalidOperationExceptionThrown() + public void GetClone_ClassInitializerNotSpecified_InvalidOperationExceptionThrown() { - var source = (AbstractClass)new DerivedClass() { AbstractProperty = 10 }; + NoDefaultConstructorClass source = new NoDefaultConstructorClass(10); var target = CloneFactory.GetClone(source); } @@ -57,7 +57,7 @@ public void GetCLone_AbstractClassInitializerSpecified_InstanceCloned() [ExpectedException(typeof(InvalidOperationException))] public void GetClone_InterfaceInitializerNotSpecified_InvalidOperationExceptionThrown() { - IInterface source = new DerivedClass() { InterfaceProperty = 10 }; + INoDefaultConstructor source = new NoDefaultConstructorClass(10); var target = CloneFactory.GetClone(source); } @@ -133,5 +133,23 @@ class CircularReference2 { public CircularReference1 Other { get;set; } } + + interface INoDefaultConstructor + { + } + + class NoDefaultConstructorClass : INoDefaultConstructor + { + public NoDefaultConstructorClass(int propOne) + { + PropOne = PropOne; + } + + private NoDefaultConstructorClass() + { + } + + public int PropOne { get; set; } + } } } diff --git a/src/CloneExtensions.UnitTests/Helpers/RandGen.cs b/src/CloneExtensions.UnitTests/Helpers/RandGen.cs new file mode 100644 index 0000000..c73ecde --- /dev/null +++ b/src/CloneExtensions.UnitTests/Helpers/RandGen.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CloneExtensions.UnitTests.Helpers +{ + public static class RandGen + { + #region Field Members + private static string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; + private static Random _rand = new Random(DateTime.Now.Millisecond); + #endregion + + #region Public Members + public static string GenerateString( + uint length) + { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < length; i++) + { + sb.Append(_chars[(int)(_rand.NextDouble() * _chars.Length)]); + } + + return sb.ToString(); + } + + public static List GenerateStringList( + uint listLength, + uint stringLength) + { + List list = new List(); + + for (int i = 0; i < listLength; i++) + { + list.Add(GenerateString(stringLength)); + } + + return list; + } + + public static int GenerateInt( + int min = int.MinValue, + int max = int.MaxValue) + { + return _rand.Next(min, max); + } + + public static List GenerateIntList( + uint listLength, + int min = int.MinValue, + int max = int.MaxValue) + { + List list = new List(); + + for (int i = 0; i < listLength; i++) + { + list.Add(GenerateInt(min, max)); + } + + return list; + } + + public static DateTime? GenerateNullableDate( + uint daysFromNow) + { + var days = _rand.Next((int)daysFromNow); + + if (days % 2 == 0) + { + return DateTime.UtcNow.AddDays(days); + } + + return null; + } + + public static byte[] GenerateByteArray( + uint length) + { + List data = new List((int)length); + + for (int x = 0; x < length; x++) + { + data.Add((byte)_rand.Next(0, 255)); + } + + return data.ToArray(); + } + #endregion + } +} \ No newline at end of file diff --git a/src/CloneExtensions.UnitTests/Helpers/TimingHelper.cs b/src/CloneExtensions.UnitTests/Helpers/TimingHelper.cs new file mode 100644 index 0000000..daacc0d --- /dev/null +++ b/src/CloneExtensions.UnitTests/Helpers/TimingHelper.cs @@ -0,0 +1,142 @@ +using System; +using System.Diagnostics; +using System.Text; + +namespace CloneExtensions.UnitTests.Helpers +{ + public static class TimingHelper + { + public static TimingResult TimeIt(Func func) + { + DateTime start = DateTime.Now; + + T result = func(); + + return new TimingResult() + { + Result = result, + Elapsed = DateTime.Now.Subtract(start).TotalMilliseconds + }; + } + + public static ComparisonResult ComparePerformance( + int iterationsFirst, + int iterationsSecond, + Action first, + Action second) + { + first(); + DateTime start = DateTime.Now; + for (int i = 0; i < iterationsFirst; i++) + { + first(); + } + var firstElapsed = DateTime.Now.Subtract(start).TotalMilliseconds; + + second(); + start = DateTime.Now; + for (int i = 0; i < iterationsSecond; i++) + { + second(); + } + var secondElapsed = DateTime.Now.Subtract(start).TotalMilliseconds; + + var firstOpsPerSec = ((double)iterationsFirst) / (firstElapsed / ((double)1000)); + var secondOpsPerSec = ((double)iterationsSecond) / (secondElapsed / ((double)1000)); + + return new ComparisonResult() + { + IterationsFirst = iterationsFirst, + IterationsSecond = iterationsSecond, + FirstTotalTime = firstElapsed, + SecondTotalTime = secondElapsed, + FirstOpsPerSec = firstOpsPerSec, + SecondOpsPerSec = secondOpsPerSec, + PeformanceDiff = Math.Ceiling((((firstElapsed / secondElapsed) - 1) * 100)) + }; + } + + public static PerformanceResult GetPerformance( + int iterations, + Action act) + { + Stopwatch sw = Stopwatch.StartNew(); + + for (int i = 0; i < iterations; i++) + { + act(); + } + + sw.Stop(); + var total = sw.ElapsedMilliseconds; + + var opsPerSec = ((double)iterations) / (total / ((double)1000)); + + return new PerformanceResult() + { + Ave = total / ((double)iterations), + Count = iterations, + Total = total, + OpsPerSec = opsPerSec + }; + } + } + + [DebuggerDisplay("{Elapsed} - {Result}")] + public class TimingResult + { + public double Elapsed { get; set; } + public T Result { get; set; } + } + + public class ComparisonResult + { + public int IterationsFirst { get; set; } + public int IterationsSecond { get; set; } + public double FirstTotalTime { get; set; } + public double SecondTotalTime { get; set; } + public double FirstOpsPerSec { get; set; } + public double SecondOpsPerSec { get; set; } + public double PeformanceDiff { get; set; } + + public string GetReport() + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("IterationsFirst : " + this.IterationsFirst.ToString("N0")); + sb.AppendLine("IterationsSecond : " + this.IterationsSecond.ToString("N0")); + sb.AppendLine("Total Time - First (MS): " + this.FirstTotalTime); + sb.AppendLine("Total Time - Second (MS): " + this.SecondTotalTime); + sb.AppendLine("Ops per Sec - First: " + this.FirstOpsPerSec.ToString("N3")); + sb.AppendLine("Ops per Sec - Second: " + this.SecondOpsPerSec.ToString("N3")); + + if (this.PeformanceDiff > 0) + { + sb.AppendLine("Performance Increase: " + this.PeformanceDiff + "%"); + } + else + { + sb.AppendLine("Performance Decrease: " + Math.Abs(this.PeformanceDiff) + "%"); + } + + return sb.ToString(); + } + } + + public class PerformanceResult + { + public int Count { get; set; } + public double Ave { get; set; } + public double Total { get; set; } + public double OpsPerSec { get; set; } + + public string GetReport() + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Count : " + this.Count.ToString("N0")); + sb.AppendLine("Ave : " + this.Ave.ToString("N10")); + sb.AppendLine("Total : " + this.Total); + sb.AppendLine("Ops per Sec: " + this.OpsPerSec.ToString("N3")); + return sb.ToString().Trim(); + } + } +} \ No newline at end of file diff --git a/src/CloneExtensions.UnitTests/PolymorphismSupportTests.cs b/src/CloneExtensions.UnitTests/PolymorphismSupportTests.cs new file mode 100644 index 0000000..d6f4d52 --- /dev/null +++ b/src/CloneExtensions.UnitTests/PolymorphismSupportTests.cs @@ -0,0 +1,238 @@ +using CloneExtensions.UnitTests.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CloneExtensions.UnitTests +{ + [TestClass] + public class PolymorphismSupportTests + { + [TestMethod] + public void PolymorphismSupportTests_IRealOnlyList_String() + { + IReadOnlyList source = new List() + { + RandGen.GenerateString(10) + }; + + var dest = source.GetClone(); + + Assert.IsNotNull(dest); + Assert.AreNotSame(dest, source); + Assert.AreEqual(dest.Count, source.Count); + Assert.AreEqual(dest[0], source[0]); + } + + [TestMethod] + public void PolymorphismSupportTests_Interface() + { + MyTmpInterface source = new Helper1() + { + PropOne = RandGen.GenerateInt(), + PropThree = RandGen.GenerateInt(), + PropTwo = RandGen.GenerateInt() + }; + + var target = source.GetClone(); + + Assert.IsNotNull(target); + Assert.AreNotSame(target, source); + Assert.AreEqual(target.PropOne, source.PropOne); + Assert.AreNotSame(target.PropOne, source.PropOne); + + Helper1 targetAsHelper = target as Helper1; + Helper1 sourceAsHelper = source as Helper1; + + Assert.IsNotNull(targetAsHelper); + Assert.IsNotNull(sourceAsHelper); + Assert.AreNotSame(targetAsHelper, sourceAsHelper); + Assert.AreEqual(targetAsHelper.PropOne, sourceAsHelper.PropOne); + Assert.AreEqual(targetAsHelper.PropTwo, sourceAsHelper.PropTwo); + Assert.AreEqual(targetAsHelper.PropThree, sourceAsHelper.PropThree); + Assert.AreNotSame(targetAsHelper.PropOne, sourceAsHelper.PropOne); + Assert.AreNotSame(targetAsHelper.PropTwo, sourceAsHelper.PropTwo); + Assert.AreNotSame(targetAsHelper.PropThree, sourceAsHelper.PropThree); + } + + [TestMethod] + public void PolymorphismSupportTests_IReadOnlyList_Interface() + { + IReadOnlyList source = new List() + { + new Helper1() { PropOne = RandGen.GenerateInt() }, + new Helper1_1() { PropOne = RandGen.GenerateInt() }, + }; + + var target = source.GetClone(); + + Assert.IsNotNull(target); + Assert.AreNotSame(target, source); + + Assert.IsTrue(target[0] is Helper1); + Assert.IsTrue(target[1] is Helper1_1); + Assert.AreEqual(target[0].PropOne, source[0].PropOne); + Assert.AreNotSame(target[0].PropOne, source[0].PropOne); + Assert.AreEqual(target[1].PropOne, source[1].PropOne); + Assert.AreNotSame(target[1].PropOne, source[1].PropOne); + } + + [TestMethod] + public void PolymorphismSupportTests_IReadOnlyList_Abstract() + { + IReadOnlyList source = new List() + { + new Helper1() { PropOne = RandGen.GenerateInt() }, + new Helper1_1() { PropOne = RandGen.GenerateInt() }, + }; + + var target = source.GetClone(); + + Assert.IsNotNull(target); + Assert.AreNotSame(target, source); + + Assert.IsTrue(target[0] is Helper1); + Assert.IsTrue(target[1] is Helper1_1); + Assert.AreEqual(target[0].PropOne, source[0].PropOne); + Assert.AreNotSame(target[0].PropOne, source[0].PropOne); + Assert.AreEqual(target[1].PropOne, source[1].PropOne); + Assert.AreNotSame(target[1].PropOne, source[1].PropOne); + } + + [TestMethod] + public void PolymorphismSupportTests_ConcreteSubClass() + { + Message source = new Message() + { + aRef = new Derived() + { + iBase = RandGen.GenerateInt(), + iDerived = RandGen.GenerateInt() + } + }; + + var dest = source.GetClone(); + + Assert.IsNotNull(dest); + Assert.IsNotNull(dest.aRef); + Assert.AreNotSame(dest, source); + Assert.AreNotSame(dest.aRef, source.aRef); + Assert.AreEqual(dest.aRef.iBase, source.aRef.iBase); + Assert.AreSame(dest.aRef.GetType(), source.aRef.GetType()); + Assert.AreEqual(dest.aRef.GetType(), typeof(Derived)); + } + + [TestMethod] + public void PolymorphismSupportTests_InitializerSupport() + { + // In order to remain backwards compatible, ensure + // that if a user supplied an initializer it is used + // before the new polymorphism support code is. + + int callCount = 0; + + Func initializer = (x) => + { + callCount++; + return new Helper1(); + }; + + Dictionary> initializers = new Dictionary>(); + initializers.Add(typeof(HelperAbstract), initializer); + + HelperAbstract source = new Helper1_1() + { + PropOne = RandGen.GenerateInt() + }; + + var target = source.GetClone(initializers); + + Assert.IsTrue(callCount == 1); + } + + public void PolymorphismSupportTests_SpeedComparison1() + { + Helper1 concreteSource = new Helper1() + { + PropOne = RandGen.GenerateInt(), + PropTwo = RandGen.GenerateInt(), + PropThree = RandGen.GenerateInt() + }; + + MyTmpInterface abstractSource = concreteSource as MyTmpInterface; + + var result = TimingHelper.ComparePerformance( + 10000000, + 10000000, + () => concreteSource.GetClone(), + () => abstractSource.GetClone()); + + Assert.IsFalse(true, result.GetReport()); + } + + public void PolymorphismSupportTests_SpeedComparison2() + { + List concreteSource = new List(); + + for (int i = 0; i < 10000; i++) + { + concreteSource.Add(new Helper1() + { + PropOne = RandGen.GenerateInt(), + PropTwo = RandGen.GenerateInt(), + PropThree = RandGen.GenerateInt() + }); + } + + IReadOnlyList abstractSource = concreteSource + .OfType() + .ToList(); + + var result = TimingHelper.ComparePerformance( + 1000, + 1000, + () => concreteSource.GetClone(), + () => abstractSource.GetClone()); + + Assert.IsFalse(true, result.GetReport()); + } + + #region Helpers + interface MyTmpInterface + { + int PropOne { get; set; } + } + + abstract class HelperAbstract : MyTmpInterface + { + public int PropOne { get; set; } + } + + class Helper1 : HelperAbstract + { + public int PropTwo { get; set; } + public int PropThree { get; set; } + } + + class Helper1_1 : HelperAbstract + { + } + + class Base + { + public int iBase; + } + + class Derived : Base + { + public int iDerived; + } + + class Message + { + public Base aRef; + } + #endregion + } +} \ No newline at end of file diff --git a/src/CloneExtensions/CloneItDelegateCache.cs b/src/CloneExtensions/CloneItDelegateCache.cs new file mode 100644 index 0000000..8ba2a84 --- /dev/null +++ b/src/CloneExtensions/CloneItDelegateCache.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using System.Linq; + +namespace CloneExtensions +{ + public static class CloneItDelegateCache + { + static CloneItDelegateCache() + { + _cache = new ConcurrentDictionary(); + } + + #region Field Members + private static ConcurrentDictionary _cache; + + private static MethodInfo _helper = typeof(CloneItDelegateCache) + .GetRuntimeMethods() + .Where(x => + x.Name == "CloneItDelegateHelper" && + x.IsPrivate && + x.IsStatic) + .FirstOrDefault(); + #endregion + + #region Public Members + public static CloneItDelegate Get(Type t) + { + return _cache.GetOrAdd(t, (x) => + { + return (CloneItDelegate)_helper + .MakeGenericMethod(x) + .Invoke(null, null); + }); + } + + public static CloneItDelegate Get(object source) + { + return Get(source.GetType()); + } + #endregion + + #region Private Members + private static CloneItDelegate CloneItDelegateHelper() + { + var source = Expression.Parameter(typeof(object), "source"); + var target = Expression.Variable(typeof(object), "target"); + var flags = Expression.Parameter(typeof(CloningFlags), "flags"); + var initializers = Expression.Parameter(typeof(IDictionary>), "initializers"); + var clonedObjects = Expression.Parameter(typeof(Dictionary), "clonedObjects"); + + var methodInfo = typeof(CloneManager) + .GetRuntimeMethods() + .Where(x => + x.Name == "Clone" && + x.IsStatic) + .FirstOrDefault(); + + var invoke = Expression.Call( + methodInfo, + Expression.Convert(source, typeof(T)), + flags, + initializers, + clonedObjects); + + var assign = Expression.Assign( + target, + Expression.Convert(invoke, typeof(object))); + + var block = Expression.Block( + new[] {target}, + assign, + Expression.Label(Expression.Label(typeof(object)), target)); + + return Expression.Lambda(block, source, flags, initializers, clonedObjects).Compile(); + } + #endregion + } + + public delegate object CloneItDelegate( + object source, + CloningFlags flags, + IDictionary> initializers, + Dictionary clonedObjects); +} diff --git a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs index 23a6078..77f6bfc 100644 --- a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs @@ -11,12 +11,14 @@ class ComplexTypeExpressionFactory : DeepShallowExpressionFactoryBase { Type _type; Expression _typeExpression; + private static MethodInfo _helper = typeof(CloneItDelegateCache) + .GetRuntimeMethod("Get", new[] { typeof(object) }); public ComplexTypeExpressionFactory(ParameterExpression source, Expression target, ParameterExpression flags, ParameterExpression initializers, ParameterExpression clonedObjects) : base(source, target, flags, initializers, clonedObjects) { _type = typeof(T); - _typeExpression = Expression.Constant(_type, typeof(Type)); + _typeExpression = Expression.Constant(_type, typeof(Type)); } public override bool AddNullCheck @@ -37,8 +39,28 @@ public override bool VerifyIfAlreadyClonedByReference protected override Expression GetCloneExpression(Func getItemCloneExpression) { - var initialization = GetInitializationExpression(); - var fields = + var containsKey = Expression.Variable(typeof(bool)); + var containsKeyCall = Expression.Call(Initializers, "ContainsKey", null, _typeExpression); + var assignContainsKey = Expression.Assign(containsKey, containsKeyCall); + + var ifThenElse = Expression.IfThenElse( + Expression.Or(containsKey, Expression.TypeEqual(Source, _type)), + GetAreSameTypeBlock(getItemCloneExpression, containsKey), + GetAreDiffTypeBlock()); + + return Expression.Block( + new[] { containsKey }, + assignContainsKey, + ifThenElse); + } + + private BlockExpression GetAreSameTypeBlock( + Func getItemCloneExpression, + ParameterExpression containsKey) + { + var initialization = GetInitializationExpression(containsKey); + + var fields = Expression.IfThen( Helpers.GetCloningFlagsExpression(CloningFlags.Fields, Flags), GetFieldsCloneExpression(getItemCloneExpression) @@ -54,11 +76,28 @@ protected override Expression GetCloneExpression(Func Date: Thu, 7 Sep 2017 03:55:23 -0700 Subject: [PATCH 04/15] Revert "Add support for polymorphism." This reverts commit d2035d97be1324c5abc0a2a4e3f5b40ef66298a6. --- .../CloneItDelegateCacheTests.cs | 36 --- .../ComplexTypeTests.cs | 24 +- .../Helpers/RandGen.cs | 91 ------- .../Helpers/TimingHelper.cs | 142 ----------- .../PolymorphismSupportTests.cs | 238 ------------------ src/CloneExtensions/CloneItDelegateCache.cs | 88 ------- .../ComplexTypeExpressionFactory.cs | 53 +--- 7 files changed, 10 insertions(+), 662 deletions(-) delete mode 100644 src/CloneExtensions.UnitTests/CloneItDelegateCacheTests.cs delete mode 100644 src/CloneExtensions.UnitTests/Helpers/RandGen.cs delete mode 100644 src/CloneExtensions.UnitTests/Helpers/TimingHelper.cs delete mode 100644 src/CloneExtensions.UnitTests/PolymorphismSupportTests.cs delete mode 100644 src/CloneExtensions/CloneItDelegateCache.cs diff --git a/src/CloneExtensions.UnitTests/CloneItDelegateCacheTests.cs b/src/CloneExtensions.UnitTests/CloneItDelegateCacheTests.cs deleted file mode 100644 index a7bdbe6..0000000 --- a/src/CloneExtensions.UnitTests/CloneItDelegateCacheTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -using CloneExtensions.UnitTests.Helpers; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; - -namespace CloneExtensions.UnitTests -{ - [TestClass] - public class CloneItDelegateCacheTests - { - [TestMethod] - public void CloneItDelegateCacheTests_IReadOnlyList_Int() - { - IReadOnlyList source = new List() - { - RandGen.GenerateInt() - }; - - var cloneItDelegate = CloneItDelegateCache.Get(source); - - var flags = CloningFlags.Fields | CloningFlags.Properties | CloningFlags.CollectionItems; - var initializers = new Dictionary>(); - var clonedObjects = new Dictionary(); - - var target = cloneItDelegate(source, flags, initializers, clonedObjects); - - var targetAsList = target as List; - - Assert.IsNotNull(targetAsList); - Assert.AreNotSame(targetAsList, source); - Assert.AreEqual(targetAsList.Count, source.Count); - Assert.AreEqual(targetAsList[0], source[0]); - Assert.AreNotSame(targetAsList[0], source[0]); - } - } -} \ No newline at end of file diff --git a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs index 9358ad8..c898dec 100644 --- a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs +++ b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs @@ -36,9 +36,9 @@ public void GetClone_SameTypepProperty_Cloned() [TestMethod] [ExpectedException(typeof(InvalidOperationException))] - public void GetClone_ClassInitializerNotSpecified_InvalidOperationExceptionThrown() + public void GetClone_AbstractClassInitializerNotSpecified_InvalidOperationExceptionThrown() { - NoDefaultConstructorClass source = new NoDefaultConstructorClass(10); + var source = (AbstractClass)new DerivedClass() { AbstractProperty = 10 }; var target = CloneFactory.GetClone(source); } @@ -57,7 +57,7 @@ public void GetCLone_AbstractClassInitializerSpecified_InstanceCloned() [ExpectedException(typeof(InvalidOperationException))] public void GetClone_InterfaceInitializerNotSpecified_InvalidOperationExceptionThrown() { - INoDefaultConstructor source = new NoDefaultConstructorClass(10); + IInterface source = new DerivedClass() { InterfaceProperty = 10 }; var target = CloneFactory.GetClone(source); } @@ -133,23 +133,5 @@ class CircularReference2 { public CircularReference1 Other { get;set; } } - - interface INoDefaultConstructor - { - } - - class NoDefaultConstructorClass : INoDefaultConstructor - { - public NoDefaultConstructorClass(int propOne) - { - PropOne = PropOne; - } - - private NoDefaultConstructorClass() - { - } - - public int PropOne { get; set; } - } } } diff --git a/src/CloneExtensions.UnitTests/Helpers/RandGen.cs b/src/CloneExtensions.UnitTests/Helpers/RandGen.cs deleted file mode 100644 index c73ecde..0000000 --- a/src/CloneExtensions.UnitTests/Helpers/RandGen.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CloneExtensions.UnitTests.Helpers -{ - public static class RandGen - { - #region Field Members - private static string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; - private static Random _rand = new Random(DateTime.Now.Millisecond); - #endregion - - #region Public Members - public static string GenerateString( - uint length) - { - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < length; i++) - { - sb.Append(_chars[(int)(_rand.NextDouble() * _chars.Length)]); - } - - return sb.ToString(); - } - - public static List GenerateStringList( - uint listLength, - uint stringLength) - { - List list = new List(); - - for (int i = 0; i < listLength; i++) - { - list.Add(GenerateString(stringLength)); - } - - return list; - } - - public static int GenerateInt( - int min = int.MinValue, - int max = int.MaxValue) - { - return _rand.Next(min, max); - } - - public static List GenerateIntList( - uint listLength, - int min = int.MinValue, - int max = int.MaxValue) - { - List list = new List(); - - for (int i = 0; i < listLength; i++) - { - list.Add(GenerateInt(min, max)); - } - - return list; - } - - public static DateTime? GenerateNullableDate( - uint daysFromNow) - { - var days = _rand.Next((int)daysFromNow); - - if (days % 2 == 0) - { - return DateTime.UtcNow.AddDays(days); - } - - return null; - } - - public static byte[] GenerateByteArray( - uint length) - { - List data = new List((int)length); - - for (int x = 0; x < length; x++) - { - data.Add((byte)_rand.Next(0, 255)); - } - - return data.ToArray(); - } - #endregion - } -} \ No newline at end of file diff --git a/src/CloneExtensions.UnitTests/Helpers/TimingHelper.cs b/src/CloneExtensions.UnitTests/Helpers/TimingHelper.cs deleted file mode 100644 index daacc0d..0000000 --- a/src/CloneExtensions.UnitTests/Helpers/TimingHelper.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Diagnostics; -using System.Text; - -namespace CloneExtensions.UnitTests.Helpers -{ - public static class TimingHelper - { - public static TimingResult TimeIt(Func func) - { - DateTime start = DateTime.Now; - - T result = func(); - - return new TimingResult() - { - Result = result, - Elapsed = DateTime.Now.Subtract(start).TotalMilliseconds - }; - } - - public static ComparisonResult ComparePerformance( - int iterationsFirst, - int iterationsSecond, - Action first, - Action second) - { - first(); - DateTime start = DateTime.Now; - for (int i = 0; i < iterationsFirst; i++) - { - first(); - } - var firstElapsed = DateTime.Now.Subtract(start).TotalMilliseconds; - - second(); - start = DateTime.Now; - for (int i = 0; i < iterationsSecond; i++) - { - second(); - } - var secondElapsed = DateTime.Now.Subtract(start).TotalMilliseconds; - - var firstOpsPerSec = ((double)iterationsFirst) / (firstElapsed / ((double)1000)); - var secondOpsPerSec = ((double)iterationsSecond) / (secondElapsed / ((double)1000)); - - return new ComparisonResult() - { - IterationsFirst = iterationsFirst, - IterationsSecond = iterationsSecond, - FirstTotalTime = firstElapsed, - SecondTotalTime = secondElapsed, - FirstOpsPerSec = firstOpsPerSec, - SecondOpsPerSec = secondOpsPerSec, - PeformanceDiff = Math.Ceiling((((firstElapsed / secondElapsed) - 1) * 100)) - }; - } - - public static PerformanceResult GetPerformance( - int iterations, - Action act) - { - Stopwatch sw = Stopwatch.StartNew(); - - for (int i = 0; i < iterations; i++) - { - act(); - } - - sw.Stop(); - var total = sw.ElapsedMilliseconds; - - var opsPerSec = ((double)iterations) / (total / ((double)1000)); - - return new PerformanceResult() - { - Ave = total / ((double)iterations), - Count = iterations, - Total = total, - OpsPerSec = opsPerSec - }; - } - } - - [DebuggerDisplay("{Elapsed} - {Result}")] - public class TimingResult - { - public double Elapsed { get; set; } - public T Result { get; set; } - } - - public class ComparisonResult - { - public int IterationsFirst { get; set; } - public int IterationsSecond { get; set; } - public double FirstTotalTime { get; set; } - public double SecondTotalTime { get; set; } - public double FirstOpsPerSec { get; set; } - public double SecondOpsPerSec { get; set; } - public double PeformanceDiff { get; set; } - - public string GetReport() - { - StringBuilder sb = new StringBuilder(); - sb.AppendLine("IterationsFirst : " + this.IterationsFirst.ToString("N0")); - sb.AppendLine("IterationsSecond : " + this.IterationsSecond.ToString("N0")); - sb.AppendLine("Total Time - First (MS): " + this.FirstTotalTime); - sb.AppendLine("Total Time - Second (MS): " + this.SecondTotalTime); - sb.AppendLine("Ops per Sec - First: " + this.FirstOpsPerSec.ToString("N3")); - sb.AppendLine("Ops per Sec - Second: " + this.SecondOpsPerSec.ToString("N3")); - - if (this.PeformanceDiff > 0) - { - sb.AppendLine("Performance Increase: " + this.PeformanceDiff + "%"); - } - else - { - sb.AppendLine("Performance Decrease: " + Math.Abs(this.PeformanceDiff) + "%"); - } - - return sb.ToString(); - } - } - - public class PerformanceResult - { - public int Count { get; set; } - public double Ave { get; set; } - public double Total { get; set; } - public double OpsPerSec { get; set; } - - public string GetReport() - { - StringBuilder sb = new StringBuilder(); - sb.AppendLine("Count : " + this.Count.ToString("N0")); - sb.AppendLine("Ave : " + this.Ave.ToString("N10")); - sb.AppendLine("Total : " + this.Total); - sb.AppendLine("Ops per Sec: " + this.OpsPerSec.ToString("N3")); - return sb.ToString().Trim(); - } - } -} \ No newline at end of file diff --git a/src/CloneExtensions.UnitTests/PolymorphismSupportTests.cs b/src/CloneExtensions.UnitTests/PolymorphismSupportTests.cs deleted file mode 100644 index d6f4d52..0000000 --- a/src/CloneExtensions.UnitTests/PolymorphismSupportTests.cs +++ /dev/null @@ -1,238 +0,0 @@ -using CloneExtensions.UnitTests.Helpers; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace CloneExtensions.UnitTests -{ - [TestClass] - public class PolymorphismSupportTests - { - [TestMethod] - public void PolymorphismSupportTests_IRealOnlyList_String() - { - IReadOnlyList source = new List() - { - RandGen.GenerateString(10) - }; - - var dest = source.GetClone(); - - Assert.IsNotNull(dest); - Assert.AreNotSame(dest, source); - Assert.AreEqual(dest.Count, source.Count); - Assert.AreEqual(dest[0], source[0]); - } - - [TestMethod] - public void PolymorphismSupportTests_Interface() - { - MyTmpInterface source = new Helper1() - { - PropOne = RandGen.GenerateInt(), - PropThree = RandGen.GenerateInt(), - PropTwo = RandGen.GenerateInt() - }; - - var target = source.GetClone(); - - Assert.IsNotNull(target); - Assert.AreNotSame(target, source); - Assert.AreEqual(target.PropOne, source.PropOne); - Assert.AreNotSame(target.PropOne, source.PropOne); - - Helper1 targetAsHelper = target as Helper1; - Helper1 sourceAsHelper = source as Helper1; - - Assert.IsNotNull(targetAsHelper); - Assert.IsNotNull(sourceAsHelper); - Assert.AreNotSame(targetAsHelper, sourceAsHelper); - Assert.AreEqual(targetAsHelper.PropOne, sourceAsHelper.PropOne); - Assert.AreEqual(targetAsHelper.PropTwo, sourceAsHelper.PropTwo); - Assert.AreEqual(targetAsHelper.PropThree, sourceAsHelper.PropThree); - Assert.AreNotSame(targetAsHelper.PropOne, sourceAsHelper.PropOne); - Assert.AreNotSame(targetAsHelper.PropTwo, sourceAsHelper.PropTwo); - Assert.AreNotSame(targetAsHelper.PropThree, sourceAsHelper.PropThree); - } - - [TestMethod] - public void PolymorphismSupportTests_IReadOnlyList_Interface() - { - IReadOnlyList source = new List() - { - new Helper1() { PropOne = RandGen.GenerateInt() }, - new Helper1_1() { PropOne = RandGen.GenerateInt() }, - }; - - var target = source.GetClone(); - - Assert.IsNotNull(target); - Assert.AreNotSame(target, source); - - Assert.IsTrue(target[0] is Helper1); - Assert.IsTrue(target[1] is Helper1_1); - Assert.AreEqual(target[0].PropOne, source[0].PropOne); - Assert.AreNotSame(target[0].PropOne, source[0].PropOne); - Assert.AreEqual(target[1].PropOne, source[1].PropOne); - Assert.AreNotSame(target[1].PropOne, source[1].PropOne); - } - - [TestMethod] - public void PolymorphismSupportTests_IReadOnlyList_Abstract() - { - IReadOnlyList source = new List() - { - new Helper1() { PropOne = RandGen.GenerateInt() }, - new Helper1_1() { PropOne = RandGen.GenerateInt() }, - }; - - var target = source.GetClone(); - - Assert.IsNotNull(target); - Assert.AreNotSame(target, source); - - Assert.IsTrue(target[0] is Helper1); - Assert.IsTrue(target[1] is Helper1_1); - Assert.AreEqual(target[0].PropOne, source[0].PropOne); - Assert.AreNotSame(target[0].PropOne, source[0].PropOne); - Assert.AreEqual(target[1].PropOne, source[1].PropOne); - Assert.AreNotSame(target[1].PropOne, source[1].PropOne); - } - - [TestMethod] - public void PolymorphismSupportTests_ConcreteSubClass() - { - Message source = new Message() - { - aRef = new Derived() - { - iBase = RandGen.GenerateInt(), - iDerived = RandGen.GenerateInt() - } - }; - - var dest = source.GetClone(); - - Assert.IsNotNull(dest); - Assert.IsNotNull(dest.aRef); - Assert.AreNotSame(dest, source); - Assert.AreNotSame(dest.aRef, source.aRef); - Assert.AreEqual(dest.aRef.iBase, source.aRef.iBase); - Assert.AreSame(dest.aRef.GetType(), source.aRef.GetType()); - Assert.AreEqual(dest.aRef.GetType(), typeof(Derived)); - } - - [TestMethod] - public void PolymorphismSupportTests_InitializerSupport() - { - // In order to remain backwards compatible, ensure - // that if a user supplied an initializer it is used - // before the new polymorphism support code is. - - int callCount = 0; - - Func initializer = (x) => - { - callCount++; - return new Helper1(); - }; - - Dictionary> initializers = new Dictionary>(); - initializers.Add(typeof(HelperAbstract), initializer); - - HelperAbstract source = new Helper1_1() - { - PropOne = RandGen.GenerateInt() - }; - - var target = source.GetClone(initializers); - - Assert.IsTrue(callCount == 1); - } - - public void PolymorphismSupportTests_SpeedComparison1() - { - Helper1 concreteSource = new Helper1() - { - PropOne = RandGen.GenerateInt(), - PropTwo = RandGen.GenerateInt(), - PropThree = RandGen.GenerateInt() - }; - - MyTmpInterface abstractSource = concreteSource as MyTmpInterface; - - var result = TimingHelper.ComparePerformance( - 10000000, - 10000000, - () => concreteSource.GetClone(), - () => abstractSource.GetClone()); - - Assert.IsFalse(true, result.GetReport()); - } - - public void PolymorphismSupportTests_SpeedComparison2() - { - List concreteSource = new List(); - - for (int i = 0; i < 10000; i++) - { - concreteSource.Add(new Helper1() - { - PropOne = RandGen.GenerateInt(), - PropTwo = RandGen.GenerateInt(), - PropThree = RandGen.GenerateInt() - }); - } - - IReadOnlyList abstractSource = concreteSource - .OfType() - .ToList(); - - var result = TimingHelper.ComparePerformance( - 1000, - 1000, - () => concreteSource.GetClone(), - () => abstractSource.GetClone()); - - Assert.IsFalse(true, result.GetReport()); - } - - #region Helpers - interface MyTmpInterface - { - int PropOne { get; set; } - } - - abstract class HelperAbstract : MyTmpInterface - { - public int PropOne { get; set; } - } - - class Helper1 : HelperAbstract - { - public int PropTwo { get; set; } - public int PropThree { get; set; } - } - - class Helper1_1 : HelperAbstract - { - } - - class Base - { - public int iBase; - } - - class Derived : Base - { - public int iDerived; - } - - class Message - { - public Base aRef; - } - #endregion - } -} \ No newline at end of file diff --git a/src/CloneExtensions/CloneItDelegateCache.cs b/src/CloneExtensions/CloneItDelegateCache.cs deleted file mode 100644 index 8ba2a84..0000000 --- a/src/CloneExtensions/CloneItDelegateCache.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Reflection; -using System.Linq; - -namespace CloneExtensions -{ - public static class CloneItDelegateCache - { - static CloneItDelegateCache() - { - _cache = new ConcurrentDictionary(); - } - - #region Field Members - private static ConcurrentDictionary _cache; - - private static MethodInfo _helper = typeof(CloneItDelegateCache) - .GetRuntimeMethods() - .Where(x => - x.Name == "CloneItDelegateHelper" && - x.IsPrivate && - x.IsStatic) - .FirstOrDefault(); - #endregion - - #region Public Members - public static CloneItDelegate Get(Type t) - { - return _cache.GetOrAdd(t, (x) => - { - return (CloneItDelegate)_helper - .MakeGenericMethod(x) - .Invoke(null, null); - }); - } - - public static CloneItDelegate Get(object source) - { - return Get(source.GetType()); - } - #endregion - - #region Private Members - private static CloneItDelegate CloneItDelegateHelper() - { - var source = Expression.Parameter(typeof(object), "source"); - var target = Expression.Variable(typeof(object), "target"); - var flags = Expression.Parameter(typeof(CloningFlags), "flags"); - var initializers = Expression.Parameter(typeof(IDictionary>), "initializers"); - var clonedObjects = Expression.Parameter(typeof(Dictionary), "clonedObjects"); - - var methodInfo = typeof(CloneManager) - .GetRuntimeMethods() - .Where(x => - x.Name == "Clone" && - x.IsStatic) - .FirstOrDefault(); - - var invoke = Expression.Call( - methodInfo, - Expression.Convert(source, typeof(T)), - flags, - initializers, - clonedObjects); - - var assign = Expression.Assign( - target, - Expression.Convert(invoke, typeof(object))); - - var block = Expression.Block( - new[] {target}, - assign, - Expression.Label(Expression.Label(typeof(object)), target)); - - return Expression.Lambda(block, source, flags, initializers, clonedObjects).Compile(); - } - #endregion - } - - public delegate object CloneItDelegate( - object source, - CloningFlags flags, - IDictionary> initializers, - Dictionary clonedObjects); -} diff --git a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs index 77f6bfc..23a6078 100644 --- a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs @@ -11,14 +11,12 @@ class ComplexTypeExpressionFactory : DeepShallowExpressionFactoryBase { Type _type; Expression _typeExpression; - private static MethodInfo _helper = typeof(CloneItDelegateCache) - .GetRuntimeMethod("Get", new[] { typeof(object) }); public ComplexTypeExpressionFactory(ParameterExpression source, Expression target, ParameterExpression flags, ParameterExpression initializers, ParameterExpression clonedObjects) : base(source, target, flags, initializers, clonedObjects) { _type = typeof(T); - _typeExpression = Expression.Constant(_type, typeof(Type)); + _typeExpression = Expression.Constant(_type, typeof(Type)); } public override bool AddNullCheck @@ -39,28 +37,8 @@ public override bool VerifyIfAlreadyClonedByReference protected override Expression GetCloneExpression(Func getItemCloneExpression) { - var containsKey = Expression.Variable(typeof(bool)); - var containsKeyCall = Expression.Call(Initializers, "ContainsKey", null, _typeExpression); - var assignContainsKey = Expression.Assign(containsKey, containsKeyCall); - - var ifThenElse = Expression.IfThenElse( - Expression.Or(containsKey, Expression.TypeEqual(Source, _type)), - GetAreSameTypeBlock(getItemCloneExpression, containsKey), - GetAreDiffTypeBlock()); - - return Expression.Block( - new[] { containsKey }, - assignContainsKey, - ifThenElse); - } - - private BlockExpression GetAreSameTypeBlock( - Func getItemCloneExpression, - ParameterExpression containsKey) - { - var initialization = GetInitializationExpression(containsKey); - - var fields = + var initialization = GetInitializationExpression(); + var fields = Expression.IfThen( Helpers.GetCloningFlagsExpression(CloningFlags.Fields, Flags), GetFieldsCloneExpression(getItemCloneExpression) @@ -76,28 +54,11 @@ private BlockExpression GetAreSameTypeBlock( return Expression.Block(initialization, GetAddToClonedObjectsExpression(), fields, properties, collectionItems); } - private BlockExpression GetAreDiffTypeBlock() + private Expression GetInitializationExpression() { - var func = Expression.Variable(typeof(CloneItDelegate), "func"); - - var sourceAsObject = Expression.Convert(Source, typeof(object)); - - var assignFunc = Expression.Assign( - func, - Expression.Call(_helper, sourceAsObject)); - - var assign = Expression.Assign( - Target, - Expression.Convert(Expression.Invoke(func, sourceAsObject, Flags, Initializers, ClonedObjects), _type)); - - return Expression.Block( - new[] { func }, - assignFunc, - assign); - } + // initializers.ContainsKey method call + var containsKeyCall = Expression.Call(Initializers, "ContainsKey", null, _typeExpression); - private Expression GetInitializationExpression(ParameterExpression containsKey) - { // initializer delegate invoke var dictIndex = Expression.Property(Initializers, "Item", _typeExpression); var funcInvokeCall = Expression.Call(dictIndex, "Invoke", null, Expression.Convert(Source, typeof(object))); @@ -107,7 +68,7 @@ private Expression GetInitializationExpression(ParameterExpression containsKey) var constructor = _type.GetConstructor(new Type[0]); return Expression.IfThenElse( - containsKey, + containsKeyCall, Expression.Assign(Target, initializerCall), (_type.IsAbstract() || _type.IsInterface() || (!_type.IsValueType() && constructor == null)) ? Helpers.GetThrowInvalidOperationExceptionExpression(_type) : From 5984423d77601549c1faaf3ca2e9427c35107a86 Mon Sep 17 00:00:00 2001 From: deipax Date: Thu, 7 Sep 2017 14:07:51 -0700 Subject: [PATCH 05/15] Supply a Expression factory for a List of primitives. Similar to the Array of primitives, there is no need to call the clone method for primitivies. Since List is so common, providing a custom Expression factory should be quite useful. According to my testing, here are the performance differences: List of Strings: 22,815 ops per second ->1,111,000 (about 4700% increase) List of Bytes: 46,153 ops per second ->1,199,880 (about 2500% increase) List of Ints: 44,863 ops per second -> 990,000 (about 2100% increase) List of Delegates: 1,119,194 ops per second ->5,221,409 (about 365% increase) List of DateTime: 1,274,697 ops per second -> 4,629,166 (about 263% increase) List of TimeSpan: 1,251,564 ops per second -> 5,221,409 (about 317% increase) One thing I had to do in order to address an assertion made by the unit tests was to not clone the element is the Flag.CollectionItems was missing. Which I found surpising, should there be such an assertion of Arrays of items as well? Currently the Array expression factorys do not check flags at all. --- .../ListPrimitiveTypeExpressionFactory.cs | 57 +++++++++++++++++++ src/CloneExtensions/ExpressionFactory.cs | 9 +++ 2 files changed, 66 insertions(+) create mode 100644 src/CloneExtensions/ExpressionFactories/ListPrimitiveTypeExpressionFactory.cs diff --git a/src/CloneExtensions/ExpressionFactories/ListPrimitiveTypeExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/ListPrimitiveTypeExpressionFactory.cs new file mode 100644 index 0000000..1d2ebe9 --- /dev/null +++ b/src/CloneExtensions/ExpressionFactories/ListPrimitiveTypeExpressionFactory.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace CloneExtensions.ExpressionFactories +{ + class ListPrimitiveTypeExpressionFactory : DeepShallowExpressionFactoryBase + { + private static Type _type = typeof(T); + private static ConstructorInfo _constructor = _type + .GetConstructors() + .Where(x => + x.GetParameters().Length == 1 && + x.GetParameters().ElementAt(0).ParameterType.IsGenericType()) + .FirstOrDefault(); + + private static ConstructorInfo _capacityContructor = _type + .GetConstructors() + .Where(x => + x.GetParameters().Length == 1 && + x.GetParameters().ElementAt(0).ParameterType == typeof(int)) + .FirstOrDefault(); + + public ListPrimitiveTypeExpressionFactory( + ParameterExpression source, + Expression target, + ParameterExpression flags, + ParameterExpression initializers, + ParameterExpression clonedObjects) + : base(source, target, flags, initializers, clonedObjects) + { + } + + public override bool AddNullCheck + { + get { return true; } + } + + public override bool VerifyIfAlreadyClonedByReference + { + get { return true; } + } + + protected override Expression GetCloneExpression(Func getItemCloneExpression) + { + var ifThenElse = Expression.IfThenElse( + Helpers.GetCloningFlagsExpression(CloningFlags.CollectionItems, Flags), + Expression.Assign(Target, Expression.New(_constructor, Source)), + Expression.Assign(Target, Expression.New(_capacityContructor, Expression.Property(Source, "Count")))); + + return Expression.Block( + ifThenElse, + GetAddToClonedObjectsExpression()); + } + } +} \ No newline at end of file diff --git a/src/CloneExtensions/ExpressionFactory.cs b/src/CloneExtensions/ExpressionFactory.cs index ea14d58..2756930 100644 --- a/src/CloneExtensions/ExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactory.cs @@ -140,6 +140,15 @@ private static IExpressionFactory GetExpressionFactory(ParameterExpression so { return new KeyValuePairExpressionFactory(source, target, flags, initializers, clonedObjects); } + else if (_type.IsGenericType() && _type.GetGenericTypeDefinition() == typeof(List<>)) + { + var itemType = _type.GetGenericArguments()[0]; + + if (itemType.IsPrimitiveOrKnownImmutable() || typeof(Delegate).IsAssignableFrom(itemType)) + { + return new ListPrimitiveTypeExpressionFactory(source, target, flags, initializers, clonedObjects); + } + } return new ComplexTypeExpressionFactory(source, target, flags, initializers, clonedObjects); } From b5475f77c9e89bc0a751af0dbea7a2aafaac5780 Mon Sep 17 00:00:00 2001 From: deipax Date: Mon, 11 Sep 2017 13:37:48 -0700 Subject: [PATCH 06/15] There is no need to clone any primitive types, they can be used directly. This change set removes all cloning calls for primitives. Here is a incomplete summary of the performance changes (all figures are in operations per second). Clone an Array of classes: 18,761 ->26,702 (about 42% increase) Clone an Array of Structs: 27,461 ->40,526 (about 47% increase) Clone a simple class (all primitive properties): 1,973,684 -> 2,918,287 (about 47% increase) Clone a complex class: 1,776,198 -> 2,056,202 (about 15% increase) Clone a Class:List: 0.999 -> 1.176 (about 17% increase) Clone a Class:List: 1.608 -> 3.115 (about 93% increase) Clone a KeyValue: 9,569,377 -> 14,903,129 (about 55% increase) Clone a Tuple: 3,992,015 -> 10,033,444 (about 151% increase) Clone a Nullable: 19,607,843 -> 33,025,099 (about a 68% increase) --- .../ComplexTypeExpressionFactory.cs | 14 +++++---- .../KeyValuePairExpressionFactory.cs | 18 ++++++----- .../NullableExpressionFactory.cs | 7 +++-- .../TupleExpressionFactory.cs | 30 ++++++++++++------- src/CloneExtensions/ExpressionFactory.cs | 6 ++-- src/CloneExtensions/TypeExtensions.cs | 5 ++++ 6 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs index 23a6078..8c1686c 100644 --- a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs @@ -111,7 +111,9 @@ private Expression GetMembersCloneExpression(Member[] members, Func Expression.Assign( Expression.MakeMemberAccess(Target, m.Info), - getItemCloneExpression(m.Type, Expression.MakeMemberAccess(Source, m.Info)) + m.Type.UsePrimitive() ? + Expression.MakeMemberAccess(Source, m.Info) : + getItemCloneExpression(m.Type, Expression.MakeMemberAccess(Source, m.Info)) ))); } @@ -141,6 +143,10 @@ private Expression GetForeachAddExpression(Type collectionType) var currentProperty = Expression.Property(enumerator, "Current"); var breakLabel = Expression.Label(); + var cloneItemCall = itemType.UsePrimitive() ? + currentProperty : + GetCloneMethodCall(itemType, currentProperty); + return Expression.Block( new[] { enumerator, collection }, assignToEnumerator, @@ -148,14 +154,12 @@ private Expression GetForeachAddExpression(Type collectionType) Expression.Loop( Expression.IfThenElse( Expression.NotEqual(moveNextCall, Expression.Constant(false, typeof(bool))), - Expression.Call(collection, "Add", null, - GetCloneMethodCall(itemType, currentProperty)), + Expression.Call(collection, "Add", null, cloneItemCall), Expression.Break(breakLabel) ), breakLabel ) ); } - } -} +} \ No newline at end of file diff --git a/src/CloneExtensions/ExpressionFactories/KeyValuePairExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/KeyValuePairExpressionFactory.cs index 55af83b..c1cc54c 100644 --- a/src/CloneExtensions/ExpressionFactories/KeyValuePairExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/KeyValuePairExpressionFactory.cs @@ -38,13 +38,17 @@ public override bool VerifyIfAlreadyClonedByReference protected override Expression GetCloneExpression(Func getItemCloneExpression) { - return - Expression.Assign( - Target, - Expression.New( - _constructor, - getItemCloneExpression(_keyType, Expression.Property(Source, "Key")), - getItemCloneExpression(_valueType, Expression.Property(Source, "Value")))); + var cloneKeyCall = _keyType.UsePrimitive() ? + Expression.Property(Source, "Key") : + getItemCloneExpression(_keyType, Expression.Property(Source, "Key")); + + var cloneValueCall = _valueType.UsePrimitive() ? + Expression.Property(Source, "Value") : + getItemCloneExpression(_valueType, Expression.Property(Source, "Value")); + + return Expression.Assign( + Target, + Expression.New(_constructor, cloneKeyCall, cloneValueCall)); } } } diff --git a/src/CloneExtensions/ExpressionFactories/NullableExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/NullableExpressionFactory.cs index 5d75f1d..cd71521 100644 --- a/src/CloneExtensions/ExpressionFactories/NullableExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/NullableExpressionFactory.cs @@ -40,7 +40,10 @@ public override Expression GetDeepCloneExpression() { var structType = typeof(T).GetGenericArguments()[0]; - var cloneCall = GetCloneMethodCall(structType, Expression.Property(Source, "Value")); + var cloneCall = structType.UsePrimitive() ? + Expression.Property(Source, "Value") : + GetCloneMethodCall(structType, Expression.Property(Source, "Value")); + var newNullable = Expression.New(typeof(T).GetConstructor(new[] { _structType }), cloneCall); return @@ -57,4 +60,4 @@ public override Expression GetShallowCloneExpression() return Expression.Assign(Target, Source); } } -} +} \ No newline at end of file diff --git a/src/CloneExtensions/ExpressionFactories/TupleExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/TupleExpressionFactory.cs index 6285744..57bb716 100644 --- a/src/CloneExtensions/ExpressionFactories/TupleExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/TupleExpressionFactory.cs @@ -38,20 +38,28 @@ protected override Expression GetCloneExpression(Func GetExpressionFactory(ParameterExpression source, Expression target, ParameterExpression flags, ParameterExpression initializers, ParameterExpression clonedObjects, LabelTarget returnLabel) { - if (_type.IsPrimitiveOrKnownImmutable() || typeof(Delegate).IsAssignableFrom(_type)) + if (_type.UsePrimitive()) { return new PrimitiveTypeExpressionFactory(source, target, flags, initializers, clonedObjects); } @@ -115,7 +115,7 @@ private static IExpressionFactory GetExpressionFactory(ParameterExpression so .GetGenericArguments() .First(); - if (itemType.IsPrimitiveOrKnownImmutable() || typeof(Delegate).IsAssignableFrom(itemType)) + if (itemType.UsePrimitive()) { return new ArrayPrimitiveTypeExpressionFactory(source, target, flags, initializers, clonedObjects); } @@ -144,7 +144,7 @@ private static IExpressionFactory GetExpressionFactory(ParameterExpression so { var itemType = _type.GetGenericArguments()[0]; - if (itemType.IsPrimitiveOrKnownImmutable() || typeof(Delegate).IsAssignableFrom(itemType)) + if (itemType.UsePrimitive()) { return new ListPrimitiveTypeExpressionFactory(source, target, flags, initializers, clonedObjects); } diff --git a/src/CloneExtensions/TypeExtensions.cs b/src/CloneExtensions/TypeExtensions.cs index df70d5b..da60f26 100644 --- a/src/CloneExtensions/TypeExtensions.cs +++ b/src/CloneExtensions/TypeExtensions.cs @@ -11,6 +11,11 @@ public static bool IsPrimitiveOrKnownImmutable(this Type type) return type.IsPrimitive() || CloneFactory.KnownImmutableTypes.Contains(type); } + public static bool UsePrimitive(this Type type) + { + return type.IsPrimitiveOrKnownImmutable() || typeof(Delegate).IsAssignableFrom(type); + } + #if NET40 || NET45 || NET461 public static bool IsAbstract(this Type type) { From c4fb91f02d3a56f07722aba3c4e9bd1e97b1e8e2 Mon Sep 17 00:00:00 2001 From: deipax Date: Tue, 12 Sep 2017 13:27:11 -0700 Subject: [PATCH 07/15] I am not sure if this is considered a bug or not, but Tuples are classes. I added Check for nulls. Add them to the cloned object dictionary and reuse them if found. Added unit tests to verify this functionality. --- src/CloneExtensions.UnitTests/TupleTests.cs | 29 +++++++++++++++++++ .../TupleExpressionFactory.cs | 18 ++++++------ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/CloneExtensions.UnitTests/TupleTests.cs b/src/CloneExtensions.UnitTests/TupleTests.cs index 10e75ae..b8a8f9f 100644 --- a/src/CloneExtensions.UnitTests/TupleTests.cs +++ b/src/CloneExtensions.UnitTests/TupleTests.cs @@ -1,6 +1,7 @@ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using CloneExtensions.UnitTests.Helpers; +using System.Collections.Generic; namespace CloneExtensions.UnitTests { @@ -17,6 +18,34 @@ public void GetClone_TupleOfInt_InstanceIsCloned() AssertHelpers.GetCloneAndAssert(() => new Tuple(10)); } + [TestMethod] + public void GetClone_Tuple_Null() + { + var source = (Tuple)null; + var target = CloneFactory.GetClone(source); + Assert.IsNull(target); + } + + [TestMethod] + public void GetClone_Tuple_DuplicateReuse() + { + Tuple helper = new Tuple(1); + List> source = new List>() + { + helper, + helper + }; + + var target = CloneFactory.GetClone(source); + + Assert.AreNotSame(source, target); + Assert.AreEqual(source.Count, target.Count); + Assert.AreNotSame(source[0], target[0]); + Assert.AreNotSame(source[1], target[1]); + + Assert.AreSame(target[0], target[1]); + } + [TestMethod] public void GetClone_TupleT1CloneTest_RefTypeValue_ValuesAreNotTheSame() { diff --git a/src/CloneExtensions/ExpressionFactories/TupleExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/TupleExpressionFactory.cs index 57bb716..b79cbf3 100644 --- a/src/CloneExtensions/ExpressionFactories/TupleExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/TupleExpressionFactory.cs @@ -18,7 +18,7 @@ public override bool AddNullCheck { get { - return false; + return true; } } @@ -26,7 +26,7 @@ public override bool VerifyIfAlreadyClonedByReference { get { - return false; + return true; } } @@ -63,13 +63,13 @@ protected override Expression GetCloneExpression(Func Date: Thu, 28 Sep 2017 05:38:39 -0700 Subject: [PATCH 08/15] Leverage backing fields for automatic properties. --- .../ModelInfoTests.cs | 215 ++++++++++++++++++ .../Deipax/FieldInfoExtensions.cs | 17 ++ src/CloneExtensions/Deipax/IModelInfo.cs | 32 +++ src/CloneExtensions/Deipax/ModelInfo.cs | 198 ++++++++++++++++ .../Deipax/PropertyInfoExtensions.cs | 66 ++++++ .../ComplexTypeExpressionFactory.cs | 43 +++- src/CloneExtensions/TypeExtensions.cs | 5 + 7 files changed, 564 insertions(+), 12 deletions(-) create mode 100644 src/CloneExtensions.UnitTests/ModelInfoTests.cs create mode 100644 src/CloneExtensions/Deipax/FieldInfoExtensions.cs create mode 100644 src/CloneExtensions/Deipax/IModelInfo.cs create mode 100644 src/CloneExtensions/Deipax/ModelInfo.cs create mode 100644 src/CloneExtensions/Deipax/PropertyInfoExtensions.cs diff --git a/src/CloneExtensions.UnitTests/ModelInfoTests.cs b/src/CloneExtensions.UnitTests/ModelInfoTests.cs new file mode 100644 index 0000000..38f4392 --- /dev/null +++ b/src/CloneExtensions.UnitTests/ModelInfoTests.cs @@ -0,0 +1,215 @@ +using Deipax.Core.Common; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Linq; + +namespace CloneExtensions.UnitTests +{ + [TestClass] + public class ModelInfoTests + { + [TestMethod] + public void ModelInfoTests_FieldTest() + { + AssertFields(51, 30, 15, 12, 3, 51, 39); + AssertFields(34, 20, 10, 8, 2, 34, 26); + AssertFields(17, 10, 5, 4, 1, 17, 13); + AssertFields(0, 0, 0, 0, 0, 0, 0); + } + + [TestMethod] + public void ModelInfoTests_PropertyTest() + { + AssertProperties(30, 18, 30, 6, 30, 24); + AssertProperties(20, 12, 20, 4, 20, 16); + AssertProperties(10, 6, 10, 2, 10, 8); + AssertProperties(2, 2, 0, 0, 2, 1); + } + + [TestMethod] + public void ModelInfoTests_CollectionEquivalence() + { + var m = ModelInfo.Create(typeof(GrandChildClass)); + Assert.IsTrue(ModelInfo.Fields == m.Fields); + Assert.IsTrue(ModelInfo.Properties == m.Properties); + } + + #region Private Members + private static void AssertFields( + int fieldCount, + int backingFieldCount, + int publicFieldCount, + int staticFieldCount, + int literalFieldCount, + int canReadFieldCount, + int canWriteFieldCount) + { + var fields1 = ModelInfo.Fields; + var fields2 = ModelInfo.Fields; + + var backingFields = fields1 + .Where(x => x.IsBackingField) + .ToList(); + + var publicFields = fields1 + .Where(x => x.IsPublic) + .ToList(); + + var staticFields = fields1 + .Where(x => x.IsStatic) + .ToList(); + + var literalFields = fields1 + .Where(x => x.IsLiteral) + .ToList(); + + var canReadFields = fields1 + .Where(x => x.CanRead) + .ToList(); + + var canWriteFields = fields1 + .Where(x => x.CanWrite) + .ToList(); + + Assert.IsTrue(fields1 == fields2); + Assert.IsTrue(fields1.Count() == fieldCount); + Assert.IsTrue(backingFields.Count == backingFieldCount); + Assert.IsTrue(publicFields.Count == publicFieldCount); + Assert.IsTrue(staticFields.Count == staticFieldCount); + Assert.IsTrue(literalFields.Count == literalFieldCount); + Assert.IsTrue(canReadFields.Count == canReadFieldCount); + Assert.IsTrue(canWriteFields.Count == canWriteFieldCount); + } + + private static void AssertProperties( + int propCount, + int publicPropCount, + int backingFieldCount, + int staticPropCount, + int canReadPropCount, + int canWritePropCount) + { + var props1 = ModelInfo.Properties; + var props2 = ModelInfo.Properties; + + var publicProps = props1 + .Where(x => x.IsPublic) + .ToList(); + + var backingFields = props1 + .Where(x => x.HasBackingField) + .ToList(); + + var staticProps = props1 + .Where(x => x.IsStatic) + .ToList(); + + var canReadProps = props1 + .Where(x => x.CanRead) + .ToList(); + + var canWriteProps = props1 + .Where(x => x.CanWrite) + .ToList(); + + var hasParameters = props1 + .Where(x => x.HasParameters) + .ToList(); + + Assert.IsTrue(props1 == props2); + Assert.IsTrue(props1.Count() == propCount); + Assert.IsTrue(publicProps.Count == publicPropCount); + Assert.IsTrue(backingFields.Count == backingFieldCount); + Assert.IsTrue(staticProps.Count == staticPropCount); + Assert.IsTrue(canReadProps.Count == canReadPropCount); + Assert.IsTrue(canWriteProps.Count == canWritePropCount); + Assert.IsTrue(hasParameters.Count == 0); + } + #endregion + + #region Helpers + class GrandChildClass : ChildAbstractClass + { + public new string PublicFieldCommonName; + private string PrivateFieldCommonName; + + private string PrivatePropCommonName { get; set; } + public new string PublicPropCommonName { get; set; } + + public string GrandChild_PublicField; + private string GrandChild_PrivateField; + public readonly string GrandChild_ReadOnlyField; + public static string GrandChild_StaticField; + public const string GrandChild_ConstField = ""; + + public static string GrandChild_Public_Static_GetSet_AutoProp { get; set; } + private static string GrandChild_Private_Static_GetSet_AutoProp { get; set; } + + public string GrandChild_Public_GetSet_AutoProp { get; set; } + public string GrandChild_Public_GetPSet_AutoProp { get; private set; } + public string GrandChild_Public_PGetSet_AutoProp { private get; set; } + public string GrandChild_Public_Get_AutoProp { get; } + + private string GrandChild_Private_GetSet_AutoProp { get; set; } + private string GrandChild_Private_Get_AutoProp { get; } + } + + abstract class ChildAbstractClass : ParentAbstractClass + { + public new string PublicFieldCommonName; + private string PrivateFieldCommonName; + + private string PrivatePropCommonName { get; set; } + public new string PublicPropCommonName { get; set; } + + public string Child_PublicField; + private string Child_PrivateField; + public readonly string Child_ReadOnlyField; + public static string Child_StaticField; + public const string Child_ConstField = ""; + + public static string Child_Public_Static_GetSet_AutoProp { get; set; } + private static string Child_Private_Static_GetSet_AutoProp { get; set; } + + public string Child_Public_GetSet_AutoProp { get; set; } + public string Child_Public_GetPSet_AutoProp { get; private set; } + public string Child_Public_PGetSet_AutoProp { private get; set; } + public string Child_Public_Get_AutoProp { get; } + + private string Child_Private_GetSet_AutoProp { get; set; } + private string Child_Private_Get_AutoProp { get; } + } + + abstract class ParentAbstractClass : MyInterface + { + public string PublicFieldCommonName; + private string PrivateFieldCommonName; + + private string PrivatePropCommonName { get; set; } + public string PublicPropCommonName { get; set; } + + public string Parent_PublicField; + private string Parent_PrivateField; + public readonly string Parent_ReadOnlyField; + public static string Parent_StaticField; + public const string Parent_ConstField = ""; + + public static string Parent_Public_Static_GetSet_AutoProp { get; set; } + private static string Parent_Private_Static_GetSet_AutoProp { get; set; } + + public string Parent_Public_GetSet_AutoProp { get; set; } + public string Parent_Public_GetPSet_AutoProp { get; private set; } + public string Parent_Public_PGetSet_AutoProp { private get; set; } + public string Parent_Public_Get_AutoProp { get; } + + private string Parent_Private_GetSet_AutoProp { get; set; } + private string Parent_Private_Get_AutoProp { get; } + } + + interface MyInterface + { + string Parent_Public_GetSet_AutoProp { get; set; } + string Parent_Public_Get_AutoProp { get; } + } + #endregion + } +} diff --git a/src/CloneExtensions/Deipax/FieldInfoExtensions.cs b/src/CloneExtensions/Deipax/FieldInfoExtensions.cs new file mode 100644 index 0000000..2eeb8b7 --- /dev/null +++ b/src/CloneExtensions/Deipax/FieldInfoExtensions.cs @@ -0,0 +1,17 @@ +using System.Reflection; + +namespace Deipax.Core.Extensions +{ + public static class FieldInfoExtensions + { + public static bool IsBackingField(this FieldInfo source, bool defaultValue = false) + { + if (source != null) + { + return source.Name.IndexOf(">k__BackingField", 0) >= 0; + } + + return defaultValue; + } + } +} \ No newline at end of file diff --git a/src/CloneExtensions/Deipax/IModelInfo.cs b/src/CloneExtensions/Deipax/IModelInfo.cs new file mode 100644 index 0000000..1151fc2 --- /dev/null +++ b/src/CloneExtensions/Deipax/IModelInfo.cs @@ -0,0 +1,32 @@ +using System; +using System.Reflection; + +namespace Deipax.Core.Interfaces +{ + public interface IModelInfo + { + MemberInfo MemberInfo { get; } + string Name { get; } + Type Type { get; } + bool IsStatic { get; } + bool IsPublic { get; } + bool CanRead { get; } + bool CanWrite { get; } + int Depth { get; } + bool IsLiteral { get; } + } + + public interface IFieldModelInfo : IModelInfo + { + FieldInfo FieldInfo { get; } + bool IsBackingField { get; } + } + + public interface IPropertyModelInfo : IModelInfo + { + IFieldModelInfo BackingField { get; } + PropertyInfo PropertyInfo { get; } + bool HasParameters { get; } + bool HasBackingField { get; } + } +} \ No newline at end of file diff --git a/src/CloneExtensions/Deipax/ModelInfo.cs b/src/CloneExtensions/Deipax/ModelInfo.cs new file mode 100644 index 0000000..4ccc622 --- /dev/null +++ b/src/CloneExtensions/Deipax/ModelInfo.cs @@ -0,0 +1,198 @@ +using CloneExtensions; +using Deipax.Core.Extensions; +using Deipax.Core.Interfaces; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace Deipax.Core.Common +{ + public class ModelInfo + { + static ModelInfo() + { + _cache = new ConcurrentDictionary(); + } + + private ModelInfo(Type t) + { + this.Type = t; + this.Fields = GetAllFields(this.Type); + this.Properties = GetAllProperties(this, this.Type); + } + + private ModelInfo() + { + } + + #region Field Members + private static ConcurrentDictionary _cache; + #endregion + + #region Public Members + public static ModelInfo Create(Type t) + { + if (t != null) + { + return _cache.GetOrAdd(t, (x) => + { + return new ModelInfo(x); + }); + } + + return null; + } + + public Type Type { get; private set; } + public IReadOnlyList Fields { get; private set; } + public IReadOnlyList Properties { get; private set; } + #endregion + + #region Private Members + private IFieldModelInfo GetBackingField(PropertyInfo info, int depth) + { + string key = string.Format("<{0}>k__BackingField", info.Name); + + return this.Fields + .Where(x => + x.IsBackingField && + string.Equals(x.FieldInfo.Name, key) && + x.FieldInfo.DeclaringType == info.DeclaringType && + x.Depth == depth) + .FirstOrDefault(); + } + + private static IReadOnlyList GetAllFields(Type t, int depth = 0) + { + if (t == null) + { + return new List(); + } + + var fields = t + .GetTypeInfo() + .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance) + .Where(x => x.DeclaringType == t) + .Select(x => FieldModelInfo.Create(x, depth)) + .ToList(); + + fields.AddRange(GetAllFields(t.BaseType(), ++depth)); + + return fields; + } + + private static IReadOnlyList GetAllProperties(ModelInfo m, Type t, int depth = 0) + { + if (t == null) + { + return new List(); + } + + var props = t + .GetTypeInfo() + .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance) + .Where(x => x.DeclaringType == t) + .Select(x => PropertyModelInfo.Create(m, x, depth)) + .ToList(); + + props.AddRange(GetAllProperties(m, t.BaseType(), ++depth)); + + return props; + } + #endregion + + #region Helpers + [DebuggerDisplay("{Name} - {IsStatic} - {IsPublic} - {CanRead} - {CanWrite} - {IsBackingField} - {Depth}")] + class FieldModelInfo : IFieldModelInfo + { + public static IFieldModelInfo Create(FieldInfo info, int depth) + { + return new FieldModelInfo() + { + Name = info.Name, + Type = info.FieldType, + FieldInfo = info, + MemberInfo = info, + IsStatic = info.IsStatic, + IsPublic = info.IsPublic, + CanRead = true, + CanWrite = !info.IsInitOnly && !info.IsLiteral, + IsBackingField = info.IsBackingField(false), + Depth = depth, + IsLiteral = info.IsLiteral + }; + } + + public string Name { get; private set; } + public Type Type { get; private set; } + public MemberInfo MemberInfo { get; private set; } + public FieldInfo FieldInfo { get; private set; } + public bool IsStatic { get; private set; } + public bool IsPublic { get; private set; } + public bool CanRead { get; private set; } + public bool CanWrite { get; private set; } + public bool IsBackingField { get; private set; } + public int Depth { get; private set; } + public bool IsLiteral { get; private set; } + } + + [DebuggerDisplay("{Name} - {IsStatic} - {IsPublic} - {CanRead} - {CanWrite} - {HasBackingField} - {Depth}")] + class PropertyModelInfo : IPropertyModelInfo + { + public static IPropertyModelInfo Create(ModelInfo m, PropertyInfo info, int depth) + { + var backingField = m.GetBackingField(info, depth); + + return new PropertyModelInfo() + { + Name = info.Name, + Type = info.PropertyType, + MemberInfo = info, + PropertyInfo = info, + BackingField = backingField, + IsStatic = info.IsStatic(false), + IsPublic = info.IsPublic(false), + CanRead = info.CanRead(false), + CanWrite = info.CanWrite(false), + HasParameters = info.HasParameters(false), + HasBackingField = backingField != null, + Depth = depth, + IsLiteral = false + }; + } + + public string Name { get; private set; } + public Type Type { get; private set; } + public MemberInfo MemberInfo { get; private set; } + public PropertyInfo PropertyInfo { get; private set; } + public IFieldModelInfo BackingField { get; private set; } + public bool IsStatic { get; private set; } + public bool IsPublic { get; private set; } + public bool CanRead { get; private set; } + public bool CanWrite { get; private set; } + public bool HasParameters { get; private set; } + public bool HasBackingField { get; private set; } + public int Depth { get; private set; } + public bool IsLiteral { get; private set; } + } + #endregion + } + + public static class ModelInfo + { + static ModelInfo() + { + var modelInfo = ModelInfo.Create(typeof(T)); + Fields = modelInfo.Fields; + Properties = modelInfo.Properties; + } + + #region Public Members + public static IReadOnlyList Fields { get; private set; } + public static IReadOnlyList Properties { get; private set; } + #endregion + } +} \ No newline at end of file diff --git a/src/CloneExtensions/Deipax/PropertyInfoExtensions.cs b/src/CloneExtensions/Deipax/PropertyInfoExtensions.cs new file mode 100644 index 0000000..f6f768d --- /dev/null +++ b/src/CloneExtensions/Deipax/PropertyInfoExtensions.cs @@ -0,0 +1,66 @@ +using System.Linq; +using System.Reflection; + +namespace Deipax.Core.Extensions +{ + public static class PropertyInfoExtensions + { + public static bool IsStatic(this PropertyInfo source, bool defaultValue = false) + { + if (source != null) + { + return + ((source.CanRead && source.GetMethod.IsStatic) || + (source.CanWrite && source.SetMethod.IsStatic)); + } + + return defaultValue; + } + + public static bool IsPublic(this PropertyInfo source, bool defaultValue = false) + { + if (source != null) + { + return + ((source.CanRead && source.GetMethod.IsPublic) || + (source.CanWrite && source.SetMethod.IsPublic)); + } + + return defaultValue; + } + + public static bool CanRead(this PropertyInfo source, bool defaultValue = false) + { + if (source != null) + { + return + source.CanRead && + source.GetMethod != null; + } + + return defaultValue; + } + + public static bool CanWrite(this PropertyInfo source, bool defaultValue = false) + { + if (source != null) + { + return + source.CanWrite && + source.SetMethod != null; + } + + return defaultValue; + } + + public static bool HasParameters(this PropertyInfo source, bool defaultValue = false) + { + if (source != null) + { + return source.GetIndexParameters().Any(); + } + + return defaultValue; + } + } +} \ No newline at end of file diff --git a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs index 8c1686c..0204c81 100644 --- a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs @@ -1,4 +1,5 @@ -using System; +using Deipax.Core.Common; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -81,23 +82,41 @@ private Expression GetInitializationExpression() private Expression GetFieldsCloneExpression(Func getItemCloneExpression) { - var fields = from f in _type.GetTypeInfo().GetFields(BindingFlags.Public | BindingFlags.Instance) - where !f.GetCustomAttributes(typeof(NonClonedAttribute), true).Any() - where !f.IsInitOnly - select new Member(f, f.FieldType); + var fields = ModelInfo + .Fields + .Where(x => + x.CanRead && + x.CanWrite && + x.IsPublic && + !x.IsStatic && + !x.IsBackingField && + x.MemberInfo.GetCustomAttributes(typeof(NonClonedAttribute), true).Count() == 0) + .Select(x => new Member(x.MemberInfo, x.Type)) + .ToList(); return GetMembersCloneExpression(fields.ToArray(), getItemCloneExpression); } private Expression GetPropertiesCloneExpression(Func getItemCloneExpression) { - // get all public properties with public setter and getter, which are not indexed properties - var properties = from p in _type.GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.Instance) - let setMethod = p.GetSetMethod(false) - let getMethod = p.GetGetMethod(false) - where !p.GetCustomAttributes(typeof(NonClonedAttribute), true).Any() - where setMethod != null && getMethod != null && !p.GetIndexParameters().Any() - select new Member(p, p.PropertyType); + var properties = ModelInfo + .Properties + .Where(x => + x.CanRead && + x.CanWrite && + x.IsPublic && + !x.IsStatic && + !x.HasParameters && + x.MemberInfo.GetCustomAttributes(typeof(NonClonedAttribute), true).Count() == 0) + .Select(x => new + { + Type = x.Type, + MemberInfo = x.HasBackingField ? + (MemberInfo)x.BackingField.FieldInfo : + (MemberInfo)x.PropertyInfo + }) + .Select(x => new Member(x.MemberInfo, x.Type)) + .ToList(); return GetMembersCloneExpression(properties.ToArray(), getItemCloneExpression); } diff --git a/src/CloneExtensions/TypeExtensions.cs b/src/CloneExtensions/TypeExtensions.cs index da60f26..8ef432f 100644 --- a/src/CloneExtensions/TypeExtensions.cs +++ b/src/CloneExtensions/TypeExtensions.cs @@ -16,6 +16,11 @@ public static bool UsePrimitive(this Type type) return type.IsPrimitiveOrKnownImmutable() || typeof(Delegate).IsAssignableFrom(type); } + public static Type BaseType(this Type type) + { + return type.GetTypeInfo().BaseType; + } + #if NET40 || NET45 || NET461 public static bool IsAbstract(this Type type) { From b02b431b758504d01dcc75d43dd7e5f196c194f5 Mon Sep 17 00:00:00 2001 From: deipax Date: Mon, 23 Oct 2017 04:33:57 -0700 Subject: [PATCH 09/15] Revert "Leverage backing fields for automatic properties." This reverts commit af1740c72279db8a88f27ef1105188c5a8bc387e. --- .../ModelInfoTests.cs | 215 ------------------ .../Deipax/FieldInfoExtensions.cs | 17 -- src/CloneExtensions/Deipax/IModelInfo.cs | 32 --- src/CloneExtensions/Deipax/ModelInfo.cs | 198 ---------------- .../Deipax/PropertyInfoExtensions.cs | 66 ------ .../ComplexTypeExpressionFactory.cs | 43 +--- src/CloneExtensions/TypeExtensions.cs | 5 - 7 files changed, 12 insertions(+), 564 deletions(-) delete mode 100644 src/CloneExtensions.UnitTests/ModelInfoTests.cs delete mode 100644 src/CloneExtensions/Deipax/FieldInfoExtensions.cs delete mode 100644 src/CloneExtensions/Deipax/IModelInfo.cs delete mode 100644 src/CloneExtensions/Deipax/ModelInfo.cs delete mode 100644 src/CloneExtensions/Deipax/PropertyInfoExtensions.cs diff --git a/src/CloneExtensions.UnitTests/ModelInfoTests.cs b/src/CloneExtensions.UnitTests/ModelInfoTests.cs deleted file mode 100644 index 38f4392..0000000 --- a/src/CloneExtensions.UnitTests/ModelInfoTests.cs +++ /dev/null @@ -1,215 +0,0 @@ -using Deipax.Core.Common; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Linq; - -namespace CloneExtensions.UnitTests -{ - [TestClass] - public class ModelInfoTests - { - [TestMethod] - public void ModelInfoTests_FieldTest() - { - AssertFields(51, 30, 15, 12, 3, 51, 39); - AssertFields(34, 20, 10, 8, 2, 34, 26); - AssertFields(17, 10, 5, 4, 1, 17, 13); - AssertFields(0, 0, 0, 0, 0, 0, 0); - } - - [TestMethod] - public void ModelInfoTests_PropertyTest() - { - AssertProperties(30, 18, 30, 6, 30, 24); - AssertProperties(20, 12, 20, 4, 20, 16); - AssertProperties(10, 6, 10, 2, 10, 8); - AssertProperties(2, 2, 0, 0, 2, 1); - } - - [TestMethod] - public void ModelInfoTests_CollectionEquivalence() - { - var m = ModelInfo.Create(typeof(GrandChildClass)); - Assert.IsTrue(ModelInfo.Fields == m.Fields); - Assert.IsTrue(ModelInfo.Properties == m.Properties); - } - - #region Private Members - private static void AssertFields( - int fieldCount, - int backingFieldCount, - int publicFieldCount, - int staticFieldCount, - int literalFieldCount, - int canReadFieldCount, - int canWriteFieldCount) - { - var fields1 = ModelInfo.Fields; - var fields2 = ModelInfo.Fields; - - var backingFields = fields1 - .Where(x => x.IsBackingField) - .ToList(); - - var publicFields = fields1 - .Where(x => x.IsPublic) - .ToList(); - - var staticFields = fields1 - .Where(x => x.IsStatic) - .ToList(); - - var literalFields = fields1 - .Where(x => x.IsLiteral) - .ToList(); - - var canReadFields = fields1 - .Where(x => x.CanRead) - .ToList(); - - var canWriteFields = fields1 - .Where(x => x.CanWrite) - .ToList(); - - Assert.IsTrue(fields1 == fields2); - Assert.IsTrue(fields1.Count() == fieldCount); - Assert.IsTrue(backingFields.Count == backingFieldCount); - Assert.IsTrue(publicFields.Count == publicFieldCount); - Assert.IsTrue(staticFields.Count == staticFieldCount); - Assert.IsTrue(literalFields.Count == literalFieldCount); - Assert.IsTrue(canReadFields.Count == canReadFieldCount); - Assert.IsTrue(canWriteFields.Count == canWriteFieldCount); - } - - private static void AssertProperties( - int propCount, - int publicPropCount, - int backingFieldCount, - int staticPropCount, - int canReadPropCount, - int canWritePropCount) - { - var props1 = ModelInfo.Properties; - var props2 = ModelInfo.Properties; - - var publicProps = props1 - .Where(x => x.IsPublic) - .ToList(); - - var backingFields = props1 - .Where(x => x.HasBackingField) - .ToList(); - - var staticProps = props1 - .Where(x => x.IsStatic) - .ToList(); - - var canReadProps = props1 - .Where(x => x.CanRead) - .ToList(); - - var canWriteProps = props1 - .Where(x => x.CanWrite) - .ToList(); - - var hasParameters = props1 - .Where(x => x.HasParameters) - .ToList(); - - Assert.IsTrue(props1 == props2); - Assert.IsTrue(props1.Count() == propCount); - Assert.IsTrue(publicProps.Count == publicPropCount); - Assert.IsTrue(backingFields.Count == backingFieldCount); - Assert.IsTrue(staticProps.Count == staticPropCount); - Assert.IsTrue(canReadProps.Count == canReadPropCount); - Assert.IsTrue(canWriteProps.Count == canWritePropCount); - Assert.IsTrue(hasParameters.Count == 0); - } - #endregion - - #region Helpers - class GrandChildClass : ChildAbstractClass - { - public new string PublicFieldCommonName; - private string PrivateFieldCommonName; - - private string PrivatePropCommonName { get; set; } - public new string PublicPropCommonName { get; set; } - - public string GrandChild_PublicField; - private string GrandChild_PrivateField; - public readonly string GrandChild_ReadOnlyField; - public static string GrandChild_StaticField; - public const string GrandChild_ConstField = ""; - - public static string GrandChild_Public_Static_GetSet_AutoProp { get; set; } - private static string GrandChild_Private_Static_GetSet_AutoProp { get; set; } - - public string GrandChild_Public_GetSet_AutoProp { get; set; } - public string GrandChild_Public_GetPSet_AutoProp { get; private set; } - public string GrandChild_Public_PGetSet_AutoProp { private get; set; } - public string GrandChild_Public_Get_AutoProp { get; } - - private string GrandChild_Private_GetSet_AutoProp { get; set; } - private string GrandChild_Private_Get_AutoProp { get; } - } - - abstract class ChildAbstractClass : ParentAbstractClass - { - public new string PublicFieldCommonName; - private string PrivateFieldCommonName; - - private string PrivatePropCommonName { get; set; } - public new string PublicPropCommonName { get; set; } - - public string Child_PublicField; - private string Child_PrivateField; - public readonly string Child_ReadOnlyField; - public static string Child_StaticField; - public const string Child_ConstField = ""; - - public static string Child_Public_Static_GetSet_AutoProp { get; set; } - private static string Child_Private_Static_GetSet_AutoProp { get; set; } - - public string Child_Public_GetSet_AutoProp { get; set; } - public string Child_Public_GetPSet_AutoProp { get; private set; } - public string Child_Public_PGetSet_AutoProp { private get; set; } - public string Child_Public_Get_AutoProp { get; } - - private string Child_Private_GetSet_AutoProp { get; set; } - private string Child_Private_Get_AutoProp { get; } - } - - abstract class ParentAbstractClass : MyInterface - { - public string PublicFieldCommonName; - private string PrivateFieldCommonName; - - private string PrivatePropCommonName { get; set; } - public string PublicPropCommonName { get; set; } - - public string Parent_PublicField; - private string Parent_PrivateField; - public readonly string Parent_ReadOnlyField; - public static string Parent_StaticField; - public const string Parent_ConstField = ""; - - public static string Parent_Public_Static_GetSet_AutoProp { get; set; } - private static string Parent_Private_Static_GetSet_AutoProp { get; set; } - - public string Parent_Public_GetSet_AutoProp { get; set; } - public string Parent_Public_GetPSet_AutoProp { get; private set; } - public string Parent_Public_PGetSet_AutoProp { private get; set; } - public string Parent_Public_Get_AutoProp { get; } - - private string Parent_Private_GetSet_AutoProp { get; set; } - private string Parent_Private_Get_AutoProp { get; } - } - - interface MyInterface - { - string Parent_Public_GetSet_AutoProp { get; set; } - string Parent_Public_Get_AutoProp { get; } - } - #endregion - } -} diff --git a/src/CloneExtensions/Deipax/FieldInfoExtensions.cs b/src/CloneExtensions/Deipax/FieldInfoExtensions.cs deleted file mode 100644 index 2eeb8b7..0000000 --- a/src/CloneExtensions/Deipax/FieldInfoExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Reflection; - -namespace Deipax.Core.Extensions -{ - public static class FieldInfoExtensions - { - public static bool IsBackingField(this FieldInfo source, bool defaultValue = false) - { - if (source != null) - { - return source.Name.IndexOf(">k__BackingField", 0) >= 0; - } - - return defaultValue; - } - } -} \ No newline at end of file diff --git a/src/CloneExtensions/Deipax/IModelInfo.cs b/src/CloneExtensions/Deipax/IModelInfo.cs deleted file mode 100644 index 1151fc2..0000000 --- a/src/CloneExtensions/Deipax/IModelInfo.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Reflection; - -namespace Deipax.Core.Interfaces -{ - public interface IModelInfo - { - MemberInfo MemberInfo { get; } - string Name { get; } - Type Type { get; } - bool IsStatic { get; } - bool IsPublic { get; } - bool CanRead { get; } - bool CanWrite { get; } - int Depth { get; } - bool IsLiteral { get; } - } - - public interface IFieldModelInfo : IModelInfo - { - FieldInfo FieldInfo { get; } - bool IsBackingField { get; } - } - - public interface IPropertyModelInfo : IModelInfo - { - IFieldModelInfo BackingField { get; } - PropertyInfo PropertyInfo { get; } - bool HasParameters { get; } - bool HasBackingField { get; } - } -} \ No newline at end of file diff --git a/src/CloneExtensions/Deipax/ModelInfo.cs b/src/CloneExtensions/Deipax/ModelInfo.cs deleted file mode 100644 index 4ccc622..0000000 --- a/src/CloneExtensions/Deipax/ModelInfo.cs +++ /dev/null @@ -1,198 +0,0 @@ -using CloneExtensions; -using Deipax.Core.Extensions; -using Deipax.Core.Interfaces; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; - -namespace Deipax.Core.Common -{ - public class ModelInfo - { - static ModelInfo() - { - _cache = new ConcurrentDictionary(); - } - - private ModelInfo(Type t) - { - this.Type = t; - this.Fields = GetAllFields(this.Type); - this.Properties = GetAllProperties(this, this.Type); - } - - private ModelInfo() - { - } - - #region Field Members - private static ConcurrentDictionary _cache; - #endregion - - #region Public Members - public static ModelInfo Create(Type t) - { - if (t != null) - { - return _cache.GetOrAdd(t, (x) => - { - return new ModelInfo(x); - }); - } - - return null; - } - - public Type Type { get; private set; } - public IReadOnlyList Fields { get; private set; } - public IReadOnlyList Properties { get; private set; } - #endregion - - #region Private Members - private IFieldModelInfo GetBackingField(PropertyInfo info, int depth) - { - string key = string.Format("<{0}>k__BackingField", info.Name); - - return this.Fields - .Where(x => - x.IsBackingField && - string.Equals(x.FieldInfo.Name, key) && - x.FieldInfo.DeclaringType == info.DeclaringType && - x.Depth == depth) - .FirstOrDefault(); - } - - private static IReadOnlyList GetAllFields(Type t, int depth = 0) - { - if (t == null) - { - return new List(); - } - - var fields = t - .GetTypeInfo() - .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance) - .Where(x => x.DeclaringType == t) - .Select(x => FieldModelInfo.Create(x, depth)) - .ToList(); - - fields.AddRange(GetAllFields(t.BaseType(), ++depth)); - - return fields; - } - - private static IReadOnlyList GetAllProperties(ModelInfo m, Type t, int depth = 0) - { - if (t == null) - { - return new List(); - } - - var props = t - .GetTypeInfo() - .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance) - .Where(x => x.DeclaringType == t) - .Select(x => PropertyModelInfo.Create(m, x, depth)) - .ToList(); - - props.AddRange(GetAllProperties(m, t.BaseType(), ++depth)); - - return props; - } - #endregion - - #region Helpers - [DebuggerDisplay("{Name} - {IsStatic} - {IsPublic} - {CanRead} - {CanWrite} - {IsBackingField} - {Depth}")] - class FieldModelInfo : IFieldModelInfo - { - public static IFieldModelInfo Create(FieldInfo info, int depth) - { - return new FieldModelInfo() - { - Name = info.Name, - Type = info.FieldType, - FieldInfo = info, - MemberInfo = info, - IsStatic = info.IsStatic, - IsPublic = info.IsPublic, - CanRead = true, - CanWrite = !info.IsInitOnly && !info.IsLiteral, - IsBackingField = info.IsBackingField(false), - Depth = depth, - IsLiteral = info.IsLiteral - }; - } - - public string Name { get; private set; } - public Type Type { get; private set; } - public MemberInfo MemberInfo { get; private set; } - public FieldInfo FieldInfo { get; private set; } - public bool IsStatic { get; private set; } - public bool IsPublic { get; private set; } - public bool CanRead { get; private set; } - public bool CanWrite { get; private set; } - public bool IsBackingField { get; private set; } - public int Depth { get; private set; } - public bool IsLiteral { get; private set; } - } - - [DebuggerDisplay("{Name} - {IsStatic} - {IsPublic} - {CanRead} - {CanWrite} - {HasBackingField} - {Depth}")] - class PropertyModelInfo : IPropertyModelInfo - { - public static IPropertyModelInfo Create(ModelInfo m, PropertyInfo info, int depth) - { - var backingField = m.GetBackingField(info, depth); - - return new PropertyModelInfo() - { - Name = info.Name, - Type = info.PropertyType, - MemberInfo = info, - PropertyInfo = info, - BackingField = backingField, - IsStatic = info.IsStatic(false), - IsPublic = info.IsPublic(false), - CanRead = info.CanRead(false), - CanWrite = info.CanWrite(false), - HasParameters = info.HasParameters(false), - HasBackingField = backingField != null, - Depth = depth, - IsLiteral = false - }; - } - - public string Name { get; private set; } - public Type Type { get; private set; } - public MemberInfo MemberInfo { get; private set; } - public PropertyInfo PropertyInfo { get; private set; } - public IFieldModelInfo BackingField { get; private set; } - public bool IsStatic { get; private set; } - public bool IsPublic { get; private set; } - public bool CanRead { get; private set; } - public bool CanWrite { get; private set; } - public bool HasParameters { get; private set; } - public bool HasBackingField { get; private set; } - public int Depth { get; private set; } - public bool IsLiteral { get; private set; } - } - #endregion - } - - public static class ModelInfo - { - static ModelInfo() - { - var modelInfo = ModelInfo.Create(typeof(T)); - Fields = modelInfo.Fields; - Properties = modelInfo.Properties; - } - - #region Public Members - public static IReadOnlyList Fields { get; private set; } - public static IReadOnlyList Properties { get; private set; } - #endregion - } -} \ No newline at end of file diff --git a/src/CloneExtensions/Deipax/PropertyInfoExtensions.cs b/src/CloneExtensions/Deipax/PropertyInfoExtensions.cs deleted file mode 100644 index f6f768d..0000000 --- a/src/CloneExtensions/Deipax/PropertyInfoExtensions.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Linq; -using System.Reflection; - -namespace Deipax.Core.Extensions -{ - public static class PropertyInfoExtensions - { - public static bool IsStatic(this PropertyInfo source, bool defaultValue = false) - { - if (source != null) - { - return - ((source.CanRead && source.GetMethod.IsStatic) || - (source.CanWrite && source.SetMethod.IsStatic)); - } - - return defaultValue; - } - - public static bool IsPublic(this PropertyInfo source, bool defaultValue = false) - { - if (source != null) - { - return - ((source.CanRead && source.GetMethod.IsPublic) || - (source.CanWrite && source.SetMethod.IsPublic)); - } - - return defaultValue; - } - - public static bool CanRead(this PropertyInfo source, bool defaultValue = false) - { - if (source != null) - { - return - source.CanRead && - source.GetMethod != null; - } - - return defaultValue; - } - - public static bool CanWrite(this PropertyInfo source, bool defaultValue = false) - { - if (source != null) - { - return - source.CanWrite && - source.SetMethod != null; - } - - return defaultValue; - } - - public static bool HasParameters(this PropertyInfo source, bool defaultValue = false) - { - if (source != null) - { - return source.GetIndexParameters().Any(); - } - - return defaultValue; - } - } -} \ No newline at end of file diff --git a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs index 0204c81..8c1686c 100644 --- a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs @@ -1,5 +1,4 @@ -using Deipax.Core.Common; -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -82,41 +81,23 @@ private Expression GetInitializationExpression() private Expression GetFieldsCloneExpression(Func getItemCloneExpression) { - var fields = ModelInfo - .Fields - .Where(x => - x.CanRead && - x.CanWrite && - x.IsPublic && - !x.IsStatic && - !x.IsBackingField && - x.MemberInfo.GetCustomAttributes(typeof(NonClonedAttribute), true).Count() == 0) - .Select(x => new Member(x.MemberInfo, x.Type)) - .ToList(); + var fields = from f in _type.GetTypeInfo().GetFields(BindingFlags.Public | BindingFlags.Instance) + where !f.GetCustomAttributes(typeof(NonClonedAttribute), true).Any() + where !f.IsInitOnly + select new Member(f, f.FieldType); return GetMembersCloneExpression(fields.ToArray(), getItemCloneExpression); } private Expression GetPropertiesCloneExpression(Func getItemCloneExpression) { - var properties = ModelInfo - .Properties - .Where(x => - x.CanRead && - x.CanWrite && - x.IsPublic && - !x.IsStatic && - !x.HasParameters && - x.MemberInfo.GetCustomAttributes(typeof(NonClonedAttribute), true).Count() == 0) - .Select(x => new - { - Type = x.Type, - MemberInfo = x.HasBackingField ? - (MemberInfo)x.BackingField.FieldInfo : - (MemberInfo)x.PropertyInfo - }) - .Select(x => new Member(x.MemberInfo, x.Type)) - .ToList(); + // get all public properties with public setter and getter, which are not indexed properties + var properties = from p in _type.GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.Instance) + let setMethod = p.GetSetMethod(false) + let getMethod = p.GetGetMethod(false) + where !p.GetCustomAttributes(typeof(NonClonedAttribute), true).Any() + where setMethod != null && getMethod != null && !p.GetIndexParameters().Any() + select new Member(p, p.PropertyType); return GetMembersCloneExpression(properties.ToArray(), getItemCloneExpression); } diff --git a/src/CloneExtensions/TypeExtensions.cs b/src/CloneExtensions/TypeExtensions.cs index 8ef432f..da60f26 100644 --- a/src/CloneExtensions/TypeExtensions.cs +++ b/src/CloneExtensions/TypeExtensions.cs @@ -16,11 +16,6 @@ public static bool UsePrimitive(this Type type) return type.IsPrimitiveOrKnownImmutable() || typeof(Delegate).IsAssignableFrom(type); } - public static Type BaseType(this Type type) - { - return type.GetTypeInfo().BaseType; - } - #if NET40 || NET45 || NET461 public static bool IsAbstract(this Type type) { From ed144b5758aa01f331632d3980802b0a5b17d720 Mon Sep 17 00:00:00 2001 From: deipax Date: Tue, 24 Oct 2017 11:03:42 -0700 Subject: [PATCH 10/15] Potential fix for issue #25 --- .../ComplexTypeTests.cs | 22 +++++++++++ .../ComplexTypeExpressionFactory.cs | 37 +++++++++++++------ 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs index c898dec..b183ec6 100644 --- a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs +++ b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs @@ -88,6 +88,18 @@ public void GetClone_CircularDependency_ItemsClonedCorrectly() Assert.AreSame(target.First, target.Second, "Are the same"); } + [TestMethod] + public void GetClone_DerivedTypeWithShadowedProperty_ClonnedProperly() + { + DerivedClass1 source = new DerivedClass1() { Property = 1 }; + ((BaseClass)source).Property = 2; + + var target = CloneFactory.GetClone(source); + + Assert.AreEqual(1, target.Property); + Assert.AreEqual(2, ((BaseClass)target).Property); + } + struct SimpleStruct { public int _field; @@ -133,5 +145,15 @@ class CircularReference2 { public CircularReference1 Other { get;set; } } + + class BaseClass + { + public int Property { get; set; } + } + + class DerivedClass1 : BaseClass + { + public new int Property { get; set; } + } } } diff --git a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs index 58d1042..9bfe01c 100644 --- a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs @@ -96,17 +96,9 @@ private Expression GetPropertiesCloneExpression(Funck_BackingField` in case we can use them instead of automatic properties var backingFields = GetBackingFields(_type).ToDictionary(f => new BackingFieldInfo(f.DeclaringType, f.Name)); - // get all public properties with public setter and getter, which are not indexed properties - var properties = from p in _type.GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.Instance) - let setMethod = p.GetSetMethod(false) - let getMethod = p.GetGetMethod(false) - where !p.GetCustomAttributes(typeof(NonClonedAttribute), true).Any() - where setMethod != null && getMethod != null && !p.GetIndexParameters().Any() - select p; - // use the backing fields if available, otherwise use property var members = new List(); - foreach (var property in properties) + foreach (var property in GetProperties(_type)) { FieldInfo fieldInfo; if (backingFields.TryGetValue(new BackingFieldInfo(property.DeclaringType, "<" + property.Name + ">k__BackingField"), out fieldInfo)) @@ -182,13 +174,13 @@ private Expression GetForeachAddExpression(Type collectionType) ); } - private IEnumerable GetBackingFields(Type type) + private static IEnumerable GetBackingFields(Type type) { TypeInfo typeInfo = type.GetTypeInfo(); while(typeInfo != null && typeInfo.UnderlyingSystemType != _objectType) { - foreach(var field in typeInfo.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)) + foreach (var field in typeInfo.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)) { if (field.Name.Contains(">k__BackingField") && field.DeclaringType == typeInfo.UnderlyingSystemType) yield return field; @@ -198,6 +190,29 @@ private IEnumerable GetBackingFields(Type type) } } + private static IEnumerable GetProperties(Type type) + { + TypeInfo typeInfo = type.GetTypeInfo(); + + while (typeInfo != null && typeInfo.UnderlyingSystemType != _objectType) + { + var properties = from p in typeInfo.GetProperties(BindingFlags.Public | BindingFlags.Instance) + let setMethod = p.GetSetMethod(false) + let getMethod = p.GetGetMethod(false) + where !p.GetCustomAttributes(typeof(NonClonedAttribute), true).Any() + where setMethod != null && getMethod != null && !p.GetIndexParameters().Any() + where p.DeclaringType == typeInfo.UnderlyingSystemType + select p; + + foreach (var p in properties) + { + yield return p; + } + + typeInfo = typeInfo.BaseType?.GetTypeInfo(); + } + } + private struct BackingFieldInfo : IEquatable { public Type DeclaredType { get; } From 8f164a9bac70204c9fd86a77f357e6e2418d721e Mon Sep 17 00:00:00 2001 From: deipax Date: Wed, 25 Oct 2017 09:23:17 -0700 Subject: [PATCH 11/15] Add Unit tests for Abstract and Virtual properties. --- .../ComplexTypeTests.cs | 71 +++++++++++++++++-- .../ComplexTypeExpressionFactory.cs | 6 +- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs index b183ec6..79e1f6e 100644 --- a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs +++ b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs @@ -91,13 +91,57 @@ public void GetClone_CircularDependency_ItemsClonedCorrectly() [TestMethod] public void GetClone_DerivedTypeWithShadowedProperty_ClonnedProperly() { - DerivedClass1 source = new DerivedClass1() { Property = 1 }; - ((BaseClass)source).Property = 2; + DerivedClassOne source = new DerivedClassOne() + { + Property = 1, + VirtualProperty = 2, + VirtualProperty2 = 3, + AbstractProperty = 4, + VirtualProperty3 = "test1" + }; + + Assert.AreEqual(2, ((BaseClassOne)source).VirtualProperty); + Assert.AreEqual(3, ((BaseClassOne)source).VirtualProperty2); + Assert.AreEqual(4, ((BaseClassOne)source).AbstractProperty); + Assert.AreEqual("test1", ((BaseClassOne)source).VirtualProperty3); + + ((BaseClassOne)source).Property = 5; + ((BaseClassOne)source).VirtualProperty = 6; + ((BaseClassOne)source).VirtualProperty2 = 7; + ((BaseClassOne)source).AbstractProperty = 8; + ((BaseClassOne)source).VirtualProperty3 = "test2"; + + Assert.AreEqual(1, source.Property); + Assert.AreEqual(5, ((BaseClassOne)source).Property); + + Assert.AreEqual(6, source.VirtualProperty); + Assert.AreEqual(6, ((BaseClassOne)source).VirtualProperty); + + Assert.AreEqual(7, source.VirtualProperty2); + Assert.AreEqual(7, ((BaseClassOne)source).VirtualProperty2); + + Assert.AreEqual(8, source.AbstractProperty); + Assert.AreEqual(8, ((BaseClassOne)source).AbstractProperty); + + Assert.AreEqual("test2", source.VirtualProperty3); + Assert.AreEqual("test2", ((BaseClassOne)source).VirtualProperty3); var target = CloneFactory.GetClone(source); - Assert.AreEqual(1, target.Property); - Assert.AreEqual(2, ((BaseClass)target).Property); + Assert.AreEqual(1, source.Property); + Assert.AreEqual(5, ((BaseClassOne)source).Property); + + Assert.AreEqual(6, source.VirtualProperty); + Assert.AreEqual(6, ((BaseClassOne)source).VirtualProperty); + + Assert.AreEqual(7, source.VirtualProperty2); + Assert.AreEqual(7, ((BaseClassOne)source).VirtualProperty2); + + Assert.AreEqual(8, source.AbstractProperty); + Assert.AreEqual(8, ((BaseClassOne)source).AbstractProperty); + + Assert.AreEqual("test2", source.VirtualProperty3); + Assert.AreEqual("test2", ((BaseClassOne)source).VirtualProperty3); } struct SimpleStruct @@ -146,14 +190,29 @@ class CircularReference2 public CircularReference1 Other { get;set; } } - class BaseClass + abstract class BaseClassOne { public int Property { get; set; } + virtual public int VirtualProperty { get; set; } + abstract public int AbstractProperty { get; set; } + virtual public int VirtualProperty2 { get; set; } + + virtual public string VirtualProperty3 + { + get { return _virtualProperty; } + set { _virtualProperty = string.Empty; } + } + + private string _virtualProperty; } - class DerivedClass1 : BaseClass + class DerivedClassOne : BaseClassOne { public new int Property { get; set; } + public override int AbstractProperty { get; set; } + public override int VirtualProperty { get; set; } + // use the default implementation for VirtualProperty2 + public override string VirtualProperty3 { get; set; } } } } diff --git a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs index 9bfe01c..e226c86 100644 --- a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs @@ -98,7 +98,8 @@ private Expression GetPropertiesCloneExpression(Func(); - foreach (var property in GetProperties(_type)) + var properties = GetProperties(_type); + foreach (var property in properties) { FieldInfo fieldInfo; if (backingFields.TryGetValue(new BackingFieldInfo(property.DeclaringType, "<" + property.Name + ">k__BackingField"), out fieldInfo)) @@ -196,12 +197,11 @@ private static IEnumerable GetProperties(Type type) while (typeInfo != null && typeInfo.UnderlyingSystemType != _objectType) { - var properties = from p in typeInfo.GetProperties(BindingFlags.Public | BindingFlags.Instance) + var properties = from p in typeInfo.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) let setMethod = p.GetSetMethod(false) let getMethod = p.GetGetMethod(false) where !p.GetCustomAttributes(typeof(NonClonedAttribute), true).Any() where setMethod != null && getMethod != null && !p.GetIndexParameters().Any() - where p.DeclaringType == typeInfo.UnderlyingSystemType select p; foreach (var p in properties) From 6978185217ed66d9f12e0eb132cffca784263e0b Mon Sep 17 00:00:00 2001 From: deipax Date: Wed, 25 Oct 2017 09:50:25 -0700 Subject: [PATCH 12/15] Add a "new" Field to unit test base class and sub class. --- .../ComplexTypeTests.cs | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs index 79e1f6e..c1e5061 100644 --- a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs +++ b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs @@ -93,6 +93,7 @@ public void GetClone_DerivedTypeWithShadowedProperty_ClonnedProperly() { DerivedClassOne source = new DerivedClassOne() { + MyField = 1, Property = 1, VirtualProperty = 2, VirtualProperty2 = 3, @@ -105,12 +106,16 @@ public void GetClone_DerivedTypeWithShadowedProperty_ClonnedProperly() Assert.AreEqual(4, ((BaseClassOne)source).AbstractProperty); Assert.AreEqual("test1", ((BaseClassOne)source).VirtualProperty3); + ((BaseClassOne) source).MyField = 2; ((BaseClassOne)source).Property = 5; ((BaseClassOne)source).VirtualProperty = 6; ((BaseClassOne)source).VirtualProperty2 = 7; ((BaseClassOne)source).AbstractProperty = 8; ((BaseClassOne)source).VirtualProperty3 = "test2"; + Assert.AreEqual(1, source.MyField); + Assert.AreEqual(2, ((BaseClassOne)source).MyField); + Assert.AreEqual(1, source.Property); Assert.AreEqual(5, ((BaseClassOne)source).Property); @@ -128,20 +133,23 @@ public void GetClone_DerivedTypeWithShadowedProperty_ClonnedProperly() var target = CloneFactory.GetClone(source); - Assert.AreEqual(1, source.Property); - Assert.AreEqual(5, ((BaseClassOne)source).Property); + Assert.AreEqual(1, target.MyField); + Assert.AreEqual(2, ((BaseClassOne)target).MyField); - Assert.AreEqual(6, source.VirtualProperty); - Assert.AreEqual(6, ((BaseClassOne)source).VirtualProperty); + Assert.AreEqual(1, target.Property); + Assert.AreEqual(5, ((BaseClassOne)target).Property); - Assert.AreEqual(7, source.VirtualProperty2); - Assert.AreEqual(7, ((BaseClassOne)source).VirtualProperty2); + Assert.AreEqual(6, target.VirtualProperty); + Assert.AreEqual(6, ((BaseClassOne)target).VirtualProperty); - Assert.AreEqual(8, source.AbstractProperty); - Assert.AreEqual(8, ((BaseClassOne)source).AbstractProperty); + Assert.AreEqual(7, target.VirtualProperty2); + Assert.AreEqual(7, ((BaseClassOne)target).VirtualProperty2); - Assert.AreEqual("test2", source.VirtualProperty3); - Assert.AreEqual("test2", ((BaseClassOne)source).VirtualProperty3); + Assert.AreEqual(8, target.AbstractProperty); + Assert.AreEqual(8, ((BaseClassOne)target).AbstractProperty); + + Assert.AreEqual("test2", target.VirtualProperty3); + Assert.AreEqual("test2", ((BaseClassOne)target).VirtualProperty3); } struct SimpleStruct @@ -192,6 +200,8 @@ class CircularReference2 abstract class BaseClassOne { + public int MyField; + public int Property { get; set; } virtual public int VirtualProperty { get; set; } abstract public int AbstractProperty { get; set; } @@ -208,6 +218,8 @@ virtual public string VirtualProperty3 class DerivedClassOne : BaseClassOne { + public new int MyField; + public new int Property { get; set; } public override int AbstractProperty { get; set; } public override int VirtualProperty { get; set; } From 3464307513ae03a554f072442317ce144388095f Mon Sep 17 00:00:00 2001 From: deipax Date: Wed, 25 Oct 2017 11:02:33 -0700 Subject: [PATCH 13/15] Remove redundant abstract/virtual properties, only one is needed. --- .../v15/Server/sqlite3/db.lock | 0 .../ComplexTypeTests.cs | 6 +- .../ComplexTypeExpressionFactory.cs | 57 ++++++++++++++----- 3 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 .vs/CloneExtensions/v15/Server/sqlite3/db.lock diff --git a/.vs/CloneExtensions/v15/Server/sqlite3/db.lock b/.vs/CloneExtensions/v15/Server/sqlite3/db.lock new file mode 100644 index 0000000..e69de29 diff --git a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs index c1e5061..ec1d7bf 100644 --- a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs +++ b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs @@ -198,7 +198,7 @@ class CircularReference2 public CircularReference1 Other { get;set; } } - abstract class BaseClassOne + abstract class BaseClassOne : IInterface { public int MyField; @@ -212,7 +212,9 @@ virtual public string VirtualProperty3 get { return _virtualProperty; } set { _virtualProperty = string.Empty; } } - + + public int InterfaceProperty { get; set; } + private string _virtualProperty; } diff --git a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs index e226c86..f8f6520 100644 --- a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs @@ -193,24 +193,53 @@ private static IEnumerable GetBackingFields(Type type) private static IEnumerable GetProperties(Type type) { - TypeInfo typeInfo = type.GetTypeInfo(); + List properties = new List(); - while (typeInfo != null && typeInfo.UnderlyingSystemType != _objectType) - { - var properties = from p in typeInfo.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) - let setMethod = p.GetSetMethod(false) - let getMethod = p.GetGetMethod(false) - where !p.GetCustomAttributes(typeof(NonClonedAttribute), true).Any() - where setMethod != null && getMethod != null && !p.GetIndexParameters().Any() - select p; - - foreach (var p in properties) + var allProperties = + (from p in GetAllProperties(type) + let setMethod = p.GetSetMethod(false) + select new { - yield return p; - } + Prop = p, + IsVirtualOrAbstract = setMethod.IsAbstract || setMethod.IsVirtual + }).ToList(); + + // If properties that share a name are marked as abstract + // or virtual, then only one of them is needed in order to + // set the value of the property. Any of them should + // set the value correctly, use the first one. + allProperties + .Where(x => x.IsVirtualOrAbstract) + .Select(x => x.Prop) + .GroupBy(x => x.Name) + .ToList() + .ForEach(x => properties.Add(x.First())); + + // Add all non-virtual/non-abstract properties + properties.AddRange(allProperties.Where(x => !x.IsVirtualOrAbstract).Select(x => x.Prop)); + + return properties; + } - typeInfo = typeInfo.BaseType?.GetTypeInfo(); + private static List GetAllProperties(Type type) + { + if (type == null) + { + return new List(); } + + var typeInfo = type.GetTypeInfo(); + + var properties = + (from p in typeInfo.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + let setMethod = p.GetSetMethod(false) + let getMethod = p.GetGetMethod(false) + where !p.GetCustomAttributes(typeof(NonClonedAttribute), true).Any() + where setMethod != null && getMethod != null && !p.GetIndexParameters().Any() + select p).ToList(); + + properties.AddRange(GetAllProperties(typeInfo.BaseType)); + return properties; } private struct BackingFieldInfo : IEquatable From b75a355e08dfacbaec4634269efabbbc72a46f1b Mon Sep 17 00:00:00 2001 From: deipax Date: Wed, 25 Oct 2017 11:07:28 -0700 Subject: [PATCH 14/15] delete unneeded db.lock --- .vs/CloneExtensions/v15/Server/sqlite3/db.lock | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .vs/CloneExtensions/v15/Server/sqlite3/db.lock diff --git a/.vs/CloneExtensions/v15/Server/sqlite3/db.lock b/.vs/CloneExtensions/v15/Server/sqlite3/db.lock deleted file mode 100644 index e69de29..0000000 From 977eed9e4ce07b3b6f183179a483086ca8b6e021 Mon Sep 17 00:00:00 2001 From: deipax Date: Wed, 1 Nov 2017 12:38:06 -0700 Subject: [PATCH 15/15] Remove redundant abstract/virtual properties. Use extension functions to find the correct fields/properties. --- .gitignore | 1 + .../CloneExtensions.Benchmarks.csproj | 3 + .../CollectionTests.cs | 4 +- .../ComplexTypeTests.cs | 45 ++++ .../ComplexTypeExpressionFactory.cs | 146 +++--------- .../Extensions/FieldInfoExtensions.cs | 19 ++ .../Extensions/IModelInfoExtensions.cs | 21 ++ .../Extensions/MethodInfoExtensions.cs | 20 ++ .../Extensions/PropertyInfoExtensions.cs | 118 ++++++++++ src/CloneExtensions/Interfaces/IModelInfo.cs | 35 +++ src/CloneExtensions/TypeExtensions.cs | 218 +++++++++++++++++- 11 files changed, 510 insertions(+), 120 deletions(-) create mode 100644 src/CloneExtensions/Extensions/FieldInfoExtensions.cs create mode 100644 src/CloneExtensions/Extensions/IModelInfoExtensions.cs create mode 100644 src/CloneExtensions/Extensions/MethodInfoExtensions.cs create mode 100644 src/CloneExtensions/Extensions/PropertyInfoExtensions.cs create mode 100644 src/CloneExtensions/Interfaces/IModelInfo.cs diff --git a/.gitignore b/.gitignore index f83279f..b92d86f 100644 --- a/.gitignore +++ b/.gitignore @@ -179,3 +179,4 @@ FakesAssemblies/ *.sln.ide/ BenchmarkDotNet.Artifacts/results/ .vscode/ +/.vs/CloneExtensions/v15/Server/sqlite3 diff --git a/src/CloneExtensions.Benchmarks/CloneExtensions.Benchmarks.csproj b/src/CloneExtensions.Benchmarks/CloneExtensions.Benchmarks.csproj index 04f2bc3..321f3c6 100644 --- a/src/CloneExtensions.Benchmarks/CloneExtensions.Benchmarks.csproj +++ b/src/CloneExtensions.Benchmarks/CloneExtensions.Benchmarks.csproj @@ -12,4 +12,7 @@ + + + \ No newline at end of file diff --git a/src/CloneExtensions.UnitTests/CollectionTests.cs b/src/CloneExtensions.UnitTests/CollectionTests.cs index 3e0c11e..39cceca 100644 --- a/src/CloneExtensions.UnitTests/CollectionTests.cs +++ b/src/CloneExtensions.UnitTests/CollectionTests.cs @@ -76,9 +76,7 @@ public void GetClone_DerivedTypeWithShadowedProperty_ClonnedProperly() var target = CloneFactory.GetClone(source); Assert.AreEqual(1, target.Property); - - // TODO: Make it work ... - // Assert.AreEqual(2, ((BaseClass)target).Property); + Assert.AreEqual(2, ((BaseClass)target).Property); } class MyClass diff --git a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs index ec1d7bf..cfc68d1 100644 --- a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs +++ b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs @@ -152,6 +152,31 @@ public void GetClone_DerivedTypeWithShadowedProperty_ClonnedProperly() Assert.AreEqual("test2", ((BaseClassOne)target).VirtualProperty3); } + [TestMethod] + public void GetClone_DerivedTypeWithShadowedProperty_ClonnedProperly2() + { + D source = new D() + { + Foo = "D" + }; + + ((C)source).Foo = "C"; + ((B)source).Foo = "B"; + ((A)source).Foo = "A"; + + Assert.AreEqual("C", source.Foo); + Assert.AreEqual("C", ((C)source).Foo); + Assert.AreEqual("A", ((B)source).Foo); + Assert.AreEqual("A", ((A)source).Foo); + + var target = source.GetClone(); + + Assert.AreEqual("C", target.Foo); + Assert.AreEqual("C", ((C)target).Foo); + Assert.AreEqual("A", ((B)target).Foo); + Assert.AreEqual("A", ((A)target).Foo); + } + struct SimpleStruct { public int _field; @@ -228,5 +253,25 @@ class DerivedClassOne : BaseClassOne // use the default implementation for VirtualProperty2 public override string VirtualProperty3 { get; set; } } + + public class A + { + public virtual string Foo { get; set; } + } + + public class B : A + { + public override string Foo { get; set; } + } + + public class C : B + { + public virtual new string Foo { get; set; } + } + + public class D : C + { + public override string Foo { get; set; } + } } } diff --git a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs index f8f6520..37fb54d 100644 --- a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs @@ -1,4 +1,5 @@ -using System; +using CloneExtensions.Extensions; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -83,36 +84,38 @@ private Expression GetInitializationExpression() private Expression GetFieldsCloneExpression(Func getItemCloneExpression) { - var fields = from f in _type.GetTypeInfo().GetFields(BindingFlags.Public | BindingFlags.Instance) - where !f.GetCustomAttributes(typeof(NonClonedAttribute), true).Any() - where !f.IsInitOnly - select new Member(f, f.FieldType); - - return GetMembersCloneExpression(fields.ToArray(), getItemCloneExpression); + var fields = _type + .GetAllFields() + .Where(x => + x.CanRead && + x.CanWrite && + !x.IsLiteral && + !x.IsBackingField && + x.IsPublic && + x.GetCustomAttributes().Count() == 0) + .Select(x => new Member(x.FieldInfo, x.FieldInfo.FieldType)) + .ToArray(); + + return GetMembersCloneExpression(fields, getItemCloneExpression); } private Expression GetPropertiesCloneExpression(Func getItemCloneExpression) { - // get all private fields with `>k_BackingField` in case we can use them instead of automatic properties - var backingFields = GetBackingFields(_type).ToDictionary(f => new BackingFieldInfo(f.DeclaringType, f.Name)); - - // use the backing fields if available, otherwise use property - var members = new List(); - var properties = GetProperties(_type); - foreach (var property in properties) - { - FieldInfo fieldInfo; - if (backingFields.TryGetValue(new BackingFieldInfo(property.DeclaringType, "<" + property.Name + ">k__BackingField"), out fieldInfo)) - { - members.Add(new Member(fieldInfo, fieldInfo.FieldType)); - } - else - { - members.Add(new Member(property, property.PropertyType)); - } - } - - return GetMembersCloneExpression(members.ToArray(), getItemCloneExpression); + var members = _type + .GetFilteredProperties() + .Where(x => + x.CanRead && + x.CanWrite && + x.IsPublic && + !x.HasParameters && + !x.IsLiteral && + x.GetCustomAttributes().Count() == 0) + .Select(x => x.HasBackingField ? + new Member(x.BackingField.FieldInfo, x.BackingField.FieldInfo.FieldType) : + new Member(x.PropertyInfo, x.PropertyInfo.PropertyType)) + .ToArray(); + + return GetMembersCloneExpression(members, getItemCloneExpression); } private Expression GetMembersCloneExpression(Member[] members, Func getItemCloneExpression) @@ -174,94 +177,5 @@ private Expression GetForeachAddExpression(Type collectionType) ) ); } - - private static IEnumerable GetBackingFields(Type type) - { - TypeInfo typeInfo = type.GetTypeInfo(); - - while(typeInfo != null && typeInfo.UnderlyingSystemType != _objectType) - { - foreach (var field in typeInfo.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)) - { - if (field.Name.Contains(">k__BackingField") && field.DeclaringType == typeInfo.UnderlyingSystemType) - yield return field; - } - - typeInfo = typeInfo.BaseType?.GetTypeInfo(); - } - } - - private static IEnumerable GetProperties(Type type) - { - List properties = new List(); - - var allProperties = - (from p in GetAllProperties(type) - let setMethod = p.GetSetMethod(false) - select new - { - Prop = p, - IsVirtualOrAbstract = setMethod.IsAbstract || setMethod.IsVirtual - }).ToList(); - - // If properties that share a name are marked as abstract - // or virtual, then only one of them is needed in order to - // set the value of the property. Any of them should - // set the value correctly, use the first one. - allProperties - .Where(x => x.IsVirtualOrAbstract) - .Select(x => x.Prop) - .GroupBy(x => x.Name) - .ToList() - .ForEach(x => properties.Add(x.First())); - - // Add all non-virtual/non-abstract properties - properties.AddRange(allProperties.Where(x => !x.IsVirtualOrAbstract).Select(x => x.Prop)); - - return properties; - } - - private static List GetAllProperties(Type type) - { - if (type == null) - { - return new List(); - } - - var typeInfo = type.GetTypeInfo(); - - var properties = - (from p in typeInfo.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) - let setMethod = p.GetSetMethod(false) - let getMethod = p.GetGetMethod(false) - where !p.GetCustomAttributes(typeof(NonClonedAttribute), true).Any() - where setMethod != null && getMethod != null && !p.GetIndexParameters().Any() - select p).ToList(); - - properties.AddRange(GetAllProperties(typeInfo.BaseType)); - return properties; - } - - private struct BackingFieldInfo : IEquatable - { - public Type DeclaredType { get; } - public string Name { get; set; } - - public BackingFieldInfo(Type declaringType, string name) : this() - { - DeclaredType = declaringType; - Name = name; - } - - public bool Equals(BackingFieldInfo other) - { - return other.DeclaredType == this.DeclaredType && other.Name == this.Name; - } - - public override int GetHashCode() - { - return (17 * 23 + DeclaredType.GetHashCode()) * 23 + Name.GetHashCode(); - } - } } } \ No newline at end of file diff --git a/src/CloneExtensions/Extensions/FieldInfoExtensions.cs b/src/CloneExtensions/Extensions/FieldInfoExtensions.cs new file mode 100644 index 0000000..4169835 --- /dev/null +++ b/src/CloneExtensions/Extensions/FieldInfoExtensions.cs @@ -0,0 +1,19 @@ +using System.Reflection; + +namespace CloneExtensions.Extensions +{ + public static class FieldInfoExtensions + { + public static bool IsBackingField( + this FieldInfo source, + bool defaultValue = false) + { + if (source != null) + { + return source.Name.IndexOf(">k__BackingField", 0) >= 0; + } + + return defaultValue; + } + } +} \ No newline at end of file diff --git a/src/CloneExtensions/Extensions/IModelInfoExtensions.cs b/src/CloneExtensions/Extensions/IModelInfoExtensions.cs new file mode 100644 index 0000000..28bcd5e --- /dev/null +++ b/src/CloneExtensions/Extensions/IModelInfoExtensions.cs @@ -0,0 +1,21 @@ +using CloneExtensions.Interfaces; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace CloneExtensions.Extensions +{ + public static class IModelInfoExtensions + { + public static IEnumerable GetCustomAttributes( + this IModelInfo source) where T : Attribute + { + if (source != null) + { + return source.MemberInfo.GetCustomAttributes(); + } + + return new List(); + } + } +} \ No newline at end of file diff --git a/src/CloneExtensions/Extensions/MethodInfoExtensions.cs b/src/CloneExtensions/Extensions/MethodInfoExtensions.cs new file mode 100644 index 0000000..dbee5c7 --- /dev/null +++ b/src/CloneExtensions/Extensions/MethodInfoExtensions.cs @@ -0,0 +1,20 @@ +using System.Reflection; + +namespace CloneExtensions.Extensions +{ + public static class MethodInfoExtensions + { + public static bool HasAttribute( + this MethodInfo source, + MethodAttributes attr, + bool defaultValue = false) + { + if (source != null) + { + return (source.Attributes & attr) == attr; + } + + return defaultValue; + } + } +} \ No newline at end of file diff --git a/src/CloneExtensions/Extensions/PropertyInfoExtensions.cs b/src/CloneExtensions/Extensions/PropertyInfoExtensions.cs new file mode 100644 index 0000000..260c899 --- /dev/null +++ b/src/CloneExtensions/Extensions/PropertyInfoExtensions.cs @@ -0,0 +1,118 @@ +using System.Linq; +using System.Reflection; + +namespace CloneExtensions.Extensions +{ + public static class PropertyInfoExtensions + { + public static bool IsStatic( + this PropertyInfo source, + bool defaultValue = false) + { + if (source != null) + { + return + ((source.CanRead && source.GetMethod.IsStatic) || + (source.CanWrite && source.SetMethod.IsStatic)); + } + + return defaultValue; + } + + public static bool IsPublic( + this PropertyInfo source, + bool defaultValue = false) + { + if (source != null) + { + return + ((source.CanRead && source.GetMethod.IsPublic) || + (source.CanWrite && source.SetMethod.IsPublic)); + } + + return defaultValue; + } + + public static bool CanRead( + this PropertyInfo source, + bool defaultValue = false) + { + if (source != null) + { + return + source.CanRead && + source.GetMethod != null; + } + + return defaultValue; + } + + public static bool CanWrite( + this PropertyInfo source, + bool defaultValue = false) + { + if (source != null) + { + return + source.CanWrite && + source.SetMethod != null; + } + + return defaultValue; + } + + public static bool HasParameters( + this PropertyInfo source, + bool defaultValue = false) + { + if (source != null) + { + return source.GetIndexParameters().Any(); + } + + return defaultValue; + } + + public static bool IsVitrual( + this PropertyInfo source, + bool defaultValue) + { + if (source != null) + { + return + (source.SetMethod != null && source.SetMethod.IsVirtual) || + (source.GetMethod != null && source.GetMethod.IsVirtual); + } + + return defaultValue; + } + + public static bool IsAbstract( + this PropertyInfo source, + bool defaultValue) + { + if (source != null) + { + return + (source.SetMethod != null && source.SetMethod.IsAbstract) || + (source.GetMethod != null && source.GetMethod.IsAbstract); + } + + return defaultValue; + } + + public static bool IsNew( + this PropertyInfo source, + bool defaultValue) + { + if (source != null) + { + return + (source.SetMethod != null && (source.SetMethod.HasAttribute(MethodAttributes.NewSlot))) || + (source.GetMethod != null && (source.GetMethod.HasAttribute(MethodAttributes.NewSlot))); + } + + return defaultValue; + } + } +} \ No newline at end of file diff --git a/src/CloneExtensions/Interfaces/IModelInfo.cs b/src/CloneExtensions/Interfaces/IModelInfo.cs new file mode 100644 index 0000000..2878155 --- /dev/null +++ b/src/CloneExtensions/Interfaces/IModelInfo.cs @@ -0,0 +1,35 @@ +using System; +using System.Reflection; + +namespace CloneExtensions.Interfaces +{ + public interface IModelInfo + { + MemberInfo MemberInfo { get; } + string Name { get; } + Type Type { get; } + bool IsStatic { get; } + bool IsPublic { get; } + bool CanRead { get; } + bool CanWrite { get; } + int Depth { get; } + bool IsLiteral { get; } + } + + public interface IFieldModelInfo : IModelInfo + { + FieldInfo FieldInfo { get; } + bool IsBackingField { get; } + } + + public interface IPropertyModelInfo : IModelInfo + { + IFieldModelInfo BackingField { get; } + PropertyInfo PropertyInfo { get; } + bool HasParameters { get; } + bool HasBackingField { get; } + bool IsAbstract { get; } + bool IsVirtual { get; } + bool IsNew { get; } + } +} \ No newline at end of file diff --git a/src/CloneExtensions/TypeExtensions.cs b/src/CloneExtensions/TypeExtensions.cs index da60f26..e771c53 100644 --- a/src/CloneExtensions/TypeExtensions.cs +++ b/src/CloneExtensions/TypeExtensions.cs @@ -1,4 +1,8 @@ -using System; +using CloneExtensions.Extensions; +using CloneExtensions.Interfaces; +using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; @@ -95,5 +99,217 @@ public static bool IsInterface(this Type type) return type.GetTypeInfo().IsInterface; } #endif + + public static IReadOnlyList GetAllFields(this Type type) + { + return GetAllFieldsHelper(type, 0); + } + + public static IReadOnlyList GetAllProperties( + this Type type, + IEnumerable fields = null) + { + var allFields = fields != null ? + fields : + type.GetAllFields(); + + return GetAllPropertiesHelper(allFields, type, 0); + } + + public static IReadOnlyList GetFilteredProperties( + this Type type, + IEnumerable fields = null) + { + List properties = new List(); + var allProperties = type.GetAllProperties(fields); + + // If properties that share a name are marked as abstract + // or virtual, then only one of them is needed in order to + // set/get the value of the property. + allProperties + .Where(x => x.IsAbstract || x.IsVirtual) + .Select(x => x) + .GroupBy(x => x.Name) + .ToList() + .ForEach(x => + { + // All Abstract properties are "new" + // All initial implemention of virtual properties + // are "new". Foreach "new" property + // find the property at the lowest depth. + foreach (var item in x.Where(y => y.IsNew)) + { + var itemToAdd = item; + + foreach (var p in x.Where(y => y.Depth < item.Depth).OrderByDescending(y => y.Depth)) + { + if (!p.IsNew) + { + itemToAdd = p; + } + else + { + break; + } + } + + properties.Add(itemToAdd); + } + }); + + // Add all non-virtual/non-abstract properties + properties.AddRange(allProperties.Where(x => !(x.IsAbstract || x.IsVirtual)).Select(x => x)); + + return properties; + } + + #region Private Members + private static IReadOnlyList GetAllFieldsHelper( + Type type, + int depth = 0) + { + if (type == null) + { + return new List(); + } + + var fields = type + .GetTypeInfo() + .GetFields( + BindingFlags.Public | + BindingFlags.NonPublic | + BindingFlags.Static | + BindingFlags.Instance | + BindingFlags.DeclaredOnly) + .Select(x => FieldModelInfo.Create(x, depth)) + .ToList(); + + fields.AddRange(GetAllFieldsHelper(type.GetTypeInfo().BaseType, ++depth)); + + return fields; + } + + private static IReadOnlyList GetAllPropertiesHelper( + IEnumerable fields, + Type type, + int depth = 0) + { + if (type == null) + { + return new List(); + } + + var props = type + .GetTypeInfo() + .GetProperties( + BindingFlags.Public | + BindingFlags.NonPublic | + BindingFlags.Static | + BindingFlags.Instance | + BindingFlags.DeclaredOnly) + .Select(x => PropertyModelInfo.Create(fields, x, depth)) + .ToList(); + + props.AddRange(GetAllPropertiesHelper(fields, type.GetTypeInfo().BaseType, ++depth)); + + return props; + } + #endregion + + #region Helpers + [DebuggerDisplay("{FieldInfo.FieldType.Name} {FieldInfo.Name} {FieldInfo.DeclaringType.Name}")] + class FieldModelInfo : IFieldModelInfo + { + public static IFieldModelInfo Create( + FieldInfo info, + int depth) + { + return new FieldModelInfo() + { + Name = info.Name, + Type = info.FieldType, + FieldInfo = info, + MemberInfo = info, + IsStatic = info.IsStatic, + IsPublic = info.IsPublic, + CanRead = true, + CanWrite = !info.IsInitOnly && !info.IsLiteral, + IsBackingField = info.IsBackingField(false), + Depth = depth, + IsLiteral = info.IsLiteral + }; + } + + public string Name { get; private set; } + public Type Type { get; private set; } + public MemberInfo MemberInfo { get; private set; } + public FieldInfo FieldInfo { get; private set; } + public bool IsStatic { get; private set; } + public bool IsPublic { get; private set; } + public bool CanRead { get; private set; } + public bool CanWrite { get; private set; } + public bool IsBackingField { get; private set; } + public int Depth { get; private set; } + public bool IsLiteral { get; private set; } + } + + [DebuggerDisplay("{PropertyInfo.PropertyType.Name} {PropertyInfo.Name} {PropertyInfo.DeclaringType.Name}")] + class PropertyModelInfo : IPropertyModelInfo + { + public static IPropertyModelInfo Create( + IEnumerable fields, + PropertyInfo info, + int depth) + { + if (fields == null) throw new ArgumentNullException(nameof(fields)); + string key = string.Format("<{0}>k__BackingField", info.Name); + + var backingField = fields + .Where(x => + x.IsBackingField && + string.Equals(x.FieldInfo.Name, key) && + x.FieldInfo.DeclaringType == info.DeclaringType && + x.Depth == depth) + .FirstOrDefault(); + + return new PropertyModelInfo() + { + Name = info.Name, + Type = info.PropertyType, + MemberInfo = info, + PropertyInfo = info, + BackingField = backingField, + IsStatic = info.IsStatic(false), + IsPublic = info.IsPublic(false), + CanRead = info.CanRead(false), + CanWrite = info.CanWrite(false), + HasParameters = info.HasParameters(false), + HasBackingField = backingField != null, + Depth = depth, + IsLiteral = false, + IsAbstract = info.IsAbstract(false), + IsVirtual = info.IsVitrual(false), + IsNew = info.IsNew(false) + }; + } + + public string Name { get; private set; } + public Type Type { get; private set; } + public MemberInfo MemberInfo { get; private set; } + public PropertyInfo PropertyInfo { get; private set; } + public IFieldModelInfo BackingField { get; private set; } + public bool IsStatic { get; private set; } + public bool IsPublic { get; private set; } + public bool CanRead { get; private set; } + public bool CanWrite { get; private set; } + public bool HasParameters { get; private set; } + public bool HasBackingField { get; private set; } + public int Depth { get; private set; } + public bool IsLiteral { get; private set; } + public bool IsAbstract { get; private set; } + public bool IsVirtual { get; private set; } + public bool IsNew { get; private set; } + } + #endregion } }