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