Skip to content

Commit

Permalink
support dynamic and ExpandOobject
Browse files Browse the repository at this point in the history
  • Loading branch information
lofcz committed Jan 7, 2025
1 parent d8025d2 commit b1a3c5f
Show file tree
Hide file tree
Showing 14 changed files with 343 additions and 102 deletions.
168 changes: 168 additions & 0 deletions FastCloner.Tests/SpecificScenariosTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.Tracing;
using System.Drawing;
using System.Dynamic;
using System.Globalization;
using Microsoft.EntityFrameworkCore;

Expand Down Expand Up @@ -216,6 +217,173 @@ public void Complex_Circular_Reference_Clone()
Assert.That(clonedB.Next, Is.SameAs(clonedC), "References should point to new instances");
});
}

[Test]
public void Dynamic_Object_Clone()
{
// Arrange
dynamic original = new ExpandoObject();
original.Name = "Test";
original.Number = 42;
original.Nested = new ExpandoObject();
original.Nested.Value = "Nested Value";

// Act
dynamic cloned = FastCloner.DeepClone(original);

// Assert
Assert.Multiple(() =>
{
Assert.That(cloned, Is.Not.SameAs(original), "Cloned object should be a new instance");
Assert.That(cloned.Name, Is.EqualTo("Test"), "String property should be copied");
Assert.That(cloned.Number, Is.EqualTo(42), "Number property should be copied");
Assert.That(cloned.Nested, Is.Not.SameAs(original.Nested), "Nested object should be cloned");
Assert.That(cloned.Nested.Value, Is.EqualTo("Nested Value"), "Nested value should be copied");
});
}

[Test]
public void Dynamic_With_Delegate_Clone()
{
// Arrange
dynamic original = new ExpandoObject();
int counter = 0;
original.Name = "Test";
original.Increment = (Func<int>)(() => ++counter);

// Act
dynamic cloned = FastCloner.DeepClone(original);

// Assert
Assert.Multiple(() =>
{
Assert.That(cloned.Name, Is.EqualTo("Test"), "String property should be copied");

int originalResult = original.Increment();
int clonedResult = cloned.Increment();
Assert.That(originalResult, Is.EqualTo(1), "Original delegate should increment counter");
Assert.That(clonedResult, Is.EqualTo(2), "Cloned delegate should share the same counter");
Assert.That(counter, Is.EqualTo(2), "Counter should be incremented twice");

originalResult = original.Increment();
clonedResult = cloned.Increment();
Assert.That(originalResult, Is.EqualTo(3), "Original delegate should continue counting");
Assert.That(clonedResult, Is.EqualTo(4), "Cloned delegate should continue counting");
Assert.That(counter, Is.EqualTo(4), "Counter should be incremented four times");
});
}

[Test]
public void ExpandoObject_With_Collection_Clone()
{
// Arrange
dynamic original = new ExpandoObject();
original.List = new List<string> { "Item1", "Item2" };
original.Dictionary = new Dictionary<string, int> { ["Key1"] = 1, ["Key2"] = 2 };

// Act
dynamic cloned = FastCloner.DeepClone(original);

// Assert
Assert.Multiple(() =>
{
Assert.That(cloned.List, Is.Not.SameAs(original.List), "List should be cloned");
Assert.That(cloned.List, Is.EquivalentTo(original.List), "List items should be copied");
Assert.That(cloned.Dictionary, Is.Not.SameAs(original.Dictionary), "Dictionary should be cloned");
Assert.That(cloned.Dictionary["Key1"], Is.EqualTo(1), "Dictionary values should be copied");
Assert.That(cloned.Dictionary["Key2"], Is.EqualTo(2), "Dictionary values should be copied");
});
}

[Test]
public void ExpandoObject_With_Circular_Reference_Clone()
{
// Arrange
dynamic original = new ExpandoObject();
dynamic nested = new ExpandoObject();
original.Name = "Original";
original.Nested = nested;
nested.Parent = original; // Circular reference

// Act
dynamic cloned = FastCloner.DeepClone(original);

// Assert
Assert.Multiple(() =>
{
Assert.That(cloned, Is.Not.SameAs(original), "Cloned object should be a new instance");
Assert.That(cloned.Nested, Is.Not.SameAs(original.Nested), "Nested object should be cloned");
Assert.That(cloned.Name, Is.EqualTo("Original"), "Properties should be copied");
Assert.That(cloned.Nested.Parent, Is.SameAs(cloned), "Circular reference should point to cloned instance");
});
}

[Test]
public void Mixed_Dynamic_And_Static_Types_Clone()
{
// Arrange
StaticType staticObject = new StaticType { Value = "Static" };
dynamic dynamic = new ExpandoObject();
dynamic.Static = staticObject;
dynamic.Name = "Dynamic";

// Act
dynamic cloned = FastCloner.DeepClone(dynamic);

// Assert
Assert.Multiple(() =>
{
Assert.That(cloned.Static, Is.Not.SameAs(staticObject), "Static type should be cloned");
Assert.That(cloned.Static.Value, Is.EqualTo("Static"), "Static type properties should be copied");
Assert.That(cloned.Name, Is.EqualTo("Dynamic"), "Dynamic properties should be copied");
});
}

private class StaticType
{
public string Value { get; set; }
}

[Test]
public void ExpandoObject_With_Null_Values_Clone()
{
// Arrange
dynamic original = new ExpandoObject();
original.NullProperty = null;
original.ValidProperty = "NotNull";

// Act
dynamic cloned = FastCloner.DeepClone(original);

// Assert
Assert.Multiple(() =>
{
Assert.That(((object)cloned.NullProperty), Is.Null, "Null properties should remain null");
Assert.That(cloned.ValidProperty, Is.EqualTo("NotNull"), "Non-null properties should be copied");
});
}

[Test]
public void Dynamic_Object_With_Complex_Types_Clone()
{
// Arrange
dynamic original = new ExpandoObject();
original.DateTime = DateTime.Now;
original.Guid = Guid.NewGuid();
original.TimeSpan = TimeSpan.FromHours(1);

// Act
dynamic cloned = FastCloner.DeepClone(original);

// Assert
Assert.Multiple(() =>
{
Assert.That(cloned.DateTime, Is.EqualTo(original.DateTime), "DateTime should be copied");
Assert.That(cloned.Guid, Is.EqualTo(original.Guid), "Guid should be copied");
Assert.That(cloned.TimeSpan, Is.EqualTo(original.TimeSpan), "TimeSpan should be copied");
});
}


private class Node
{
Expand Down
2 changes: 1 addition & 1 deletion FastCloner/Code/BadTypes.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace FastCloner.Helpers;
namespace FastCloner.Code;

internal class BadTypes
{
Expand Down
40 changes: 20 additions & 20 deletions FastCloner/Code/ClonerToExprGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Linq.Expressions;
using System.Reflection;

namespace FastCloner.Helpers;
namespace FastCloner.Code;

internal static class ClonerToExprGenerator
{
Expand All @@ -27,7 +27,7 @@ private static object GenerateProcessMethod(Type type, bool isDeepClone)
ParameterExpression? fromLocal = from;
ParameterExpression? to = Expression.Parameter(methodType);
ParameterExpression? toLocal = to;
ParameterExpression? state = Expression.Parameter(typeof(DeepCloneState));
ParameterExpression? state = Expression.Parameter(typeof(FastCloneState));

// if (!type.IsValueType())
{
Expand All @@ -42,7 +42,7 @@ private static object GenerateProcessMethod(Type type, bool isDeepClone)
// added from -> to binding to ensure reference loop handling
// structs cannot loop here
// state.AddKnownRef(from, to)
expressionList.Add(Expression.Call(state, typeof(DeepCloneState).GetMethod(nameof(DeepCloneState.AddKnownRef))!, from, to));
expressionList.Add(Expression.Call(state, typeof(FastCloneState).GetMethod(nameof(FastCloneState.AddKnownRef))!, from, to));
}
}

Expand All @@ -59,7 +59,7 @@ private static object GenerateProcessMethod(Type type, bool isDeepClone)

foreach (FieldInfo? fieldInfo in fi)
{
if (isDeepClone && !DeepClonerSafeTypes.CanReturnSameObject(fieldInfo.FieldType))
if (isDeepClone && !FastClonerSafeTypes.CanReturnSameObject(fieldInfo.FieldType))
{
MethodInfo? methodInfo = fieldInfo.FieldType.IsValueType()
? StaticMethodInfos.DeepClonerGeneratorMethods.CloneStructInternal.MakeGenericMethod(fieldInfo.FieldType)
Expand All @@ -78,7 +78,7 @@ private static object GenerateProcessMethod(Type type, bool isDeepClone)
{
// var setMethod = fieldInfo.GetType().GetMethod("SetValue", new[] { typeof(object), typeof(object) });
// expressionList.Add(Expression.Call(Expression.Constant(fieldInfo), setMethod, toLocal, call));
MethodInfo? setMethod = typeof(DeepClonerExprGenerator).GetPrivateStaticMethod(nameof(DeepClonerExprGenerator.ForceSetField))!;
MethodInfo? setMethod = typeof(FastClonerExprGenerator).GetPrivateStaticMethod(nameof(FastClonerExprGenerator.ForceSetField))!;
expressionList.Add(Expression.Call(setMethod, Expression.Constant(fieldInfo),
Expression.Convert(toLocal, typeof(object)), Expression.Convert(call, typeof(object))));
}
Expand All @@ -95,7 +95,7 @@ private static object GenerateProcessMethod(Type type, bool isDeepClone)

expressionList.Add(Expression.Convert(toLocal, methodType));

Type? funcType = typeof(Func<,,,>).MakeGenericType(methodType, methodType, typeof(DeepCloneState), methodType);
Type? funcType = typeof(Func<,,,>).MakeGenericType(methodType, methodType, typeof(FastCloneState), methodType);

List<ParameterExpression>? blockParams = [];
if (from != fromLocal) blockParams.Add(fromLocal);
Expand All @@ -111,9 +111,9 @@ private static object GenerateProcessArrayMethod(Type type, bool isDeep)

ParameterExpression from = Expression.Parameter(typeof(object));
ParameterExpression to = Expression.Parameter(typeof(object));
ParameterExpression? state = Expression.Parameter(typeof(DeepCloneState));
ParameterExpression? state = Expression.Parameter(typeof(FastCloneState));

Type? funcType = typeof(Func<,,,>).MakeGenericType(typeof(object), typeof(object), typeof(DeepCloneState), typeof(object));
Type? funcType = typeof(Func<,,,>).MakeGenericType(typeof(object), typeof(object), typeof(FastCloneState), typeof(object));

if (rank == 1 && type == elementType.MakeArrayType())
{
Expand All @@ -127,7 +127,7 @@ private static object GenerateProcessArrayMethod(Type type, bool isDeep)
else
{
string? methodName = nameof(Clone1DimArrayClassInternal);
if (DeepClonerSafeTypes.CanReturnSameObject(elementType)) methodName = nameof(Clone1DimArraySafeInternal);
if (FastClonerSafeTypes.CanReturnSameObject(elementType)) methodName = nameof(Clone1DimArraySafeInternal);
else if (elementType.IsValueType()) methodName = nameof(Clone1DimArrayStructInternal);
MethodInfo? methodInfo = typeof(ClonerToExprGenerator).GetPrivateStaticMethod(methodName)!.MakeGenericMethod(elementType);
MethodCallExpression? callS = Expression.Call(methodInfo, Expression.Convert(from, type), Expression.Convert(to, type), state);
Expand Down Expand Up @@ -157,40 +157,40 @@ internal static T[] ShallowClone1DimArraySafeInternal<T>(T[] objFrom, T[] objTo)
}

// when we can't use code generation, we can use these methods
internal static T[] Clone1DimArraySafeInternal<T>(T[] objFrom, T[] objTo, DeepCloneState state)
internal static T[] Clone1DimArraySafeInternal<T>(T[] objFrom, T[] objTo, FastCloneState state)
{
int l = Math.Min(objFrom.Length, objTo.Length);
state.AddKnownRef(objFrom, objTo);
Array.Copy(objFrom, objTo, l);
return objTo;
}

internal static T[]? Clone1DimArrayStructInternal<T>(T[]? objFrom, T[]? objTo, DeepCloneState state)
internal static T[]? Clone1DimArrayStructInternal<T>(T[]? objFrom, T[]? objTo, FastCloneState state)
{
// not null from called method, but will check it anyway
if (objFrom == null || objTo == null) return null;
int l = Math.Min(objFrom.Length, objTo.Length);
state.AddKnownRef(objFrom, objTo);
Func<T, DeepCloneState, T>? cloner = DeepClonerGenerator.GetClonerForValueType<T>();
Func<T, FastCloneState, T>? cloner = FastClonerGenerator.GetClonerForValueType<T>();
for (int i = 0; i < l; i++)
objTo[i] = cloner(objTo[i], state);

return objTo;
}

internal static T[]? Clone1DimArrayClassInternal<T>(T[]? objFrom, T[]? objTo, DeepCloneState state)
internal static T[]? Clone1DimArrayClassInternal<T>(T[]? objFrom, T[]? objTo, FastCloneState state)
{
// not null from called method, but will check it anyway
if (objFrom == null || objTo == null) return null;
int l = Math.Min(objFrom.Length, objTo.Length);
state.AddKnownRef(objFrom, objTo);
for (int i = 0; i < l; i++)
objTo[i] = (T)DeepClonerGenerator.CloneClassInternal(objFrom[i], state)!;
objTo[i] = (T)FastClonerGenerator.CloneClassInternal(objFrom[i], state)!;

return objTo;
}

internal static T[,]? Clone2DimArrayInternal<T>(T[,]? objFrom, T[,]? objTo, DeepCloneState state, bool isDeep)
internal static T[,]? Clone2DimArrayInternal<T>(T[,]? objFrom, T[,]? objTo, FastCloneState state, bool isDeep)
{
// not null from called method, but will check it anyway
if (objFrom == null || objTo == null) return null;
Expand All @@ -201,7 +201,7 @@ internal static T[] Clone1DimArraySafeInternal<T>(T[] objFrom, T[] objTo, DeepCl
int l1 = Math.Min(objFrom.GetLength(0), objTo.GetLength(0));
int l2 = Math.Min(objFrom.GetLength(1), objTo.GetLength(1));
state.AddKnownRef(objFrom, objTo);
if ((!isDeep || DeepClonerSafeTypes.CanReturnSameObject(typeof(T)))
if ((!isDeep || FastClonerSafeTypes.CanReturnSameObject(typeof(T)))
&& objFrom.GetLength(0) == objTo.GetLength(0)
&& objFrom.GetLength(1) == objTo.GetLength(1))
{
Expand All @@ -219,7 +219,7 @@ internal static T[] Clone1DimArraySafeInternal<T>(T[] objFrom, T[] objTo, DeepCl

if (typeof(T).IsValueType())
{
Func<T, DeepCloneState, T>? cloner = DeepClonerGenerator.GetClonerForValueType<T>();
Func<T, FastCloneState, T>? cloner = FastClonerGenerator.GetClonerForValueType<T>();
for (int i = 0; i < l1; i++)
for (int k = 0; k < l2; k++)
objTo[i, k] = cloner(objFrom[i, k], state);
Expand All @@ -228,14 +228,14 @@ internal static T[] Clone1DimArraySafeInternal<T>(T[] objFrom, T[] objTo, DeepCl
{
for (int i = 0; i < l1; i++)
for (int k = 0; k < l2; k++)
objTo[i, k] = (T)DeepClonerGenerator.CloneClassInternal(objFrom[i, k], state)!;
objTo[i, k] = (T)FastClonerGenerator.CloneClassInternal(objFrom[i, k], state)!;
}

return objTo;
}

// rare cases, very slow cloning. currently it's ok
internal static Array? CloneAbstractArrayInternal(Array? objFrom, Array? objTo, DeepCloneState state, bool isDeep)
internal static Array? CloneAbstractArrayInternal(Array? objFrom, Array? objTo, FastCloneState state, bool isDeep)
{
// not null from called method, but will check it anyway
if (objFrom == null || objTo == null) return null;
Expand All @@ -259,7 +259,7 @@ internal static T[] Clone1DimArraySafeInternal<T>(T[] objFrom, T[] objTo, DeepCl
{
objTo.SetValue(
isDeep
? DeepClonerGenerator.CloneClassInternal(
? FastClonerGenerator.CloneClassInternal(
objFrom.GetValue(idxesFrom),
state)
: objFrom.GetValue(idxesFrom), idxesTo);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System.Runtime.CompilerServices;

namespace FastCloner.Helpers;
namespace FastCloner.Code;

internal class DeepCloneState
internal class FastCloneState
{
private MiniDictionary? _loops;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System.Collections.Concurrent;

namespace FastCloner.Helpers;
namespace FastCloner.Code;

internal static class DeepClonerCache
internal static class FastClonerCache
{
private static readonly ConcurrentDictionary<Type, object> _typeCache = new ConcurrentDictionary<Type, object>();

Expand Down
Loading

0 comments on commit b1a3c5f

Please sign in to comment.