Skip to content

Commit 9c05f28

Browse files
committed
support dynamic and expandoobject
1 parent d8025d2 commit 9c05f28

14 files changed

+343
-102
lines changed

FastCloner.Tests/SpecificScenariosTest.cs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.ComponentModel.DataAnnotations.Schema;
33
using System.Diagnostics.Tracing;
44
using System.Drawing;
5+
using System.Dynamic;
56
using System.Globalization;
67
using Microsoft.EntityFrameworkCore;
78

@@ -216,6 +217,173 @@ public void Complex_Circular_Reference_Clone()
216217
Assert.That(clonedB.Next, Is.SameAs(clonedC), "References should point to new instances");
217218
});
218219
}
220+
221+
[Test]
222+
public void Dynamic_Object_Clone()
223+
{
224+
// Arrange
225+
dynamic original = new ExpandoObject();
226+
original.Name = "Test";
227+
original.Number = 42;
228+
original.Nested = new ExpandoObject();
229+
original.Nested.Value = "Nested Value";
230+
231+
// Act
232+
dynamic cloned = FastCloner.DeepClone(original);
233+
234+
// Assert
235+
Assert.Multiple(() =>
236+
{
237+
Assert.That(cloned, Is.Not.SameAs(original), "Cloned object should be a new instance");
238+
Assert.That(cloned.Name, Is.EqualTo("Test"), "String property should be copied");
239+
Assert.That(cloned.Number, Is.EqualTo(42), "Number property should be copied");
240+
Assert.That(cloned.Nested, Is.Not.SameAs(original.Nested), "Nested object should be cloned");
241+
Assert.That(cloned.Nested.Value, Is.EqualTo("Nested Value"), "Nested value should be copied");
242+
});
243+
}
244+
245+
[Test]
246+
public void Dynamic_With_Delegate_Clone()
247+
{
248+
// Arrange
249+
dynamic original = new ExpandoObject();
250+
int counter = 0;
251+
original.Name = "Test";
252+
original.Increment = (Func<int>)(() => ++counter);
253+
254+
// Act
255+
dynamic cloned = FastCloner.DeepClone(original);
256+
257+
// Assert
258+
Assert.Multiple(() =>
259+
{
260+
Assert.That(cloned.Name, Is.EqualTo("Test"), "String property should be copied");
261+
262+
int originalResult = original.Increment();
263+
int clonedResult = cloned.Increment();
264+
Assert.That(originalResult, Is.EqualTo(1), "Original delegate should increment counter");
265+
Assert.That(clonedResult, Is.EqualTo(2), "Cloned delegate should share the same counter");
266+
Assert.That(counter, Is.EqualTo(2), "Counter should be incremented twice");
267+
268+
originalResult = original.Increment();
269+
clonedResult = cloned.Increment();
270+
Assert.That(originalResult, Is.EqualTo(3), "Original delegate should continue counting");
271+
Assert.That(clonedResult, Is.EqualTo(4), "Cloned delegate should continue counting");
272+
Assert.That(counter, Is.EqualTo(4), "Counter should be incremented four times");
273+
});
274+
}
275+
276+
[Test]
277+
public void ExpandoObject_With_Collection_Clone()
278+
{
279+
// Arrange
280+
dynamic original = new ExpandoObject();
281+
original.List = new List<string> { "Item1", "Item2" };
282+
original.Dictionary = new Dictionary<string, int> { ["Key1"] = 1, ["Key2"] = 2 };
283+
284+
// Act
285+
dynamic cloned = FastCloner.DeepClone(original);
286+
287+
// Assert
288+
Assert.Multiple(() =>
289+
{
290+
Assert.That(cloned.List, Is.Not.SameAs(original.List), "List should be cloned");
291+
Assert.That(cloned.List, Is.EquivalentTo(original.List), "List items should be copied");
292+
Assert.That(cloned.Dictionary, Is.Not.SameAs(original.Dictionary), "Dictionary should be cloned");
293+
Assert.That(cloned.Dictionary["Key1"], Is.EqualTo(1), "Dictionary values should be copied");
294+
Assert.That(cloned.Dictionary["Key2"], Is.EqualTo(2), "Dictionary values should be copied");
295+
});
296+
}
297+
298+
[Test]
299+
public void ExpandoObject_With_Circular_Reference_Clone()
300+
{
301+
// Arrange
302+
dynamic original = new ExpandoObject();
303+
dynamic nested = new ExpandoObject();
304+
original.Name = "Original";
305+
original.Nested = nested;
306+
nested.Parent = original; // Circular reference
307+
308+
// Act
309+
dynamic cloned = FastCloner.DeepClone(original);
310+
311+
// Assert
312+
Assert.Multiple(() =>
313+
{
314+
Assert.That(cloned, Is.Not.SameAs(original), "Cloned object should be a new instance");
315+
Assert.That(cloned.Nested, Is.Not.SameAs(original.Nested), "Nested object should be cloned");
316+
Assert.That(cloned.Name, Is.EqualTo("Original"), "Properties should be copied");
317+
Assert.That(cloned.Nested.Parent, Is.SameAs(cloned), "Circular reference should point to cloned instance");
318+
});
319+
}
320+
321+
[Test]
322+
public void Mixed_Dynamic_And_Static_Types_Clone()
323+
{
324+
// Arrange
325+
StaticType staticObject = new StaticType { Value = "Static" };
326+
dynamic dynamic = new ExpandoObject();
327+
dynamic.Static = staticObject;
328+
dynamic.Name = "Dynamic";
329+
330+
// Act
331+
dynamic cloned = FastCloner.DeepClone(dynamic);
332+
333+
// Assert
334+
Assert.Multiple(() =>
335+
{
336+
Assert.That(cloned.Static, Is.Not.SameAs(staticObject), "Static type should be cloned");
337+
Assert.That(cloned.Static.Value, Is.EqualTo("Static"), "Static type properties should be copied");
338+
Assert.That(cloned.Name, Is.EqualTo("Dynamic"), "Dynamic properties should be copied");
339+
});
340+
}
341+
342+
private class StaticType
343+
{
344+
public string Value { get; set; }
345+
}
346+
347+
[Test]
348+
public void ExpandoObject_With_Null_Values_Clone()
349+
{
350+
// Arrange
351+
dynamic original = new ExpandoObject();
352+
original.NullProperty = null;
353+
original.ValidProperty = "NotNull";
354+
355+
// Act
356+
dynamic cloned = FastCloner.DeepClone(original);
357+
358+
// Assert
359+
Assert.Multiple(() =>
360+
{
361+
Assert.That(((object)cloned.NullProperty), Is.Null, "Null properties should remain null");
362+
Assert.That(cloned.ValidProperty, Is.EqualTo("NotNull"), "Non-null properties should be copied");
363+
});
364+
}
365+
366+
[Test]
367+
public void Dynamic_Object_With_Complex_Types_Clone()
368+
{
369+
// Arrange
370+
dynamic original = new ExpandoObject();
371+
original.DateTime = DateTime.Now;
372+
original.Guid = Guid.NewGuid();
373+
original.TimeSpan = TimeSpan.FromHours(1);
374+
375+
// Act
376+
dynamic cloned = FastCloner.DeepClone(original);
377+
378+
// Assert
379+
Assert.Multiple(() =>
380+
{
381+
Assert.That(cloned.DateTime, Is.EqualTo(original.DateTime), "DateTime should be copied");
382+
Assert.That(cloned.Guid, Is.EqualTo(original.Guid), "Guid should be copied");
383+
Assert.That(cloned.TimeSpan, Is.EqualTo(original.TimeSpan), "TimeSpan should be copied");
384+
});
385+
}
386+
219387

220388
private class Node
221389
{

FastCloner/Code/BadTypes.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace FastCloner.Helpers;
1+
namespace FastCloner.Code;
22

33
internal class BadTypes
44
{

FastCloner/Code/ClonerToExprGenerator.cs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System.Linq.Expressions;
22
using System.Reflection;
33

4-
namespace FastCloner.Helpers;
4+
namespace FastCloner.Code;
55

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

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

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

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

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

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

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

112112
ParameterExpression from = Expression.Parameter(typeof(object));
113113
ParameterExpression to = Expression.Parameter(typeof(object));
114-
ParameterExpression? state = Expression.Parameter(typeof(DeepCloneState));
114+
ParameterExpression? state = Expression.Parameter(typeof(FastCloneState));
115115

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

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

159159
// when we can't use code generation, we can use these methods
160-
internal static T[] Clone1DimArraySafeInternal<T>(T[] objFrom, T[] objTo, DeepCloneState state)
160+
internal static T[] Clone1DimArraySafeInternal<T>(T[] objFrom, T[] objTo, FastCloneState state)
161161
{
162162
int l = Math.Min(objFrom.Length, objTo.Length);
163163
state.AddKnownRef(objFrom, objTo);
164164
Array.Copy(objFrom, objTo, l);
165165
return objTo;
166166
}
167167

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

178178
return objTo;
179179
}
180180

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

190190
return objTo;
191191
}
192192

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

220220
if (typeof(T).IsValueType())
221221
{
222-
Func<T, DeepCloneState, T>? cloner = DeepClonerGenerator.GetClonerForValueType<T>();
222+
Func<T, FastCloneState, T>? cloner = FastClonerGenerator.GetClonerForValueType<T>();
223223
for (int i = 0; i < l1; i++)
224224
for (int k = 0; k < l2; k++)
225225
objTo[i, k] = cloner(objFrom[i, k], state);
@@ -228,14 +228,14 @@ internal static T[] Clone1DimArraySafeInternal<T>(T[] objFrom, T[] objTo, DeepCl
228228
{
229229
for (int i = 0; i < l1; i++)
230230
for (int k = 0; k < l2; k++)
231-
objTo[i, k] = (T)DeepClonerGenerator.CloneClassInternal(objFrom[i, k], state)!;
231+
objTo[i, k] = (T)FastClonerGenerator.CloneClassInternal(objFrom[i, k], state)!;
232232
}
233233

234234
return objTo;
235235
}
236236

237237
// rare cases, very slow cloning. currently it's ok
238-
internal static Array? CloneAbstractArrayInternal(Array? objFrom, Array? objTo, DeepCloneState state, bool isDeep)
238+
internal static Array? CloneAbstractArrayInternal(Array? objFrom, Array? objTo, FastCloneState state, bool isDeep)
239239
{
240240
// not null from called method, but will check it anyway
241241
if (objFrom == null || objTo == null) return null;
@@ -259,7 +259,7 @@ internal static T[] Clone1DimArraySafeInternal<T>(T[] objFrom, T[] objTo, DeepCl
259259
{
260260
objTo.SetValue(
261261
isDeep
262-
? DeepClonerGenerator.CloneClassInternal(
262+
? FastClonerGenerator.CloneClassInternal(
263263
objFrom.GetValue(idxesFrom),
264264
state)
265265
: objFrom.GetValue(idxesFrom), idxesTo);
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
using System.Runtime.CompilerServices;
22

3-
namespace FastCloner.Helpers;
3+
namespace FastCloner.Code;
44

5-
internal class DeepCloneState
5+
internal class FastCloneState
66
{
77
private MiniDictionary? _loops;
88

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
using System.Collections.Concurrent;
22

3-
namespace FastCloner.Helpers;
3+
namespace FastCloner.Code;
44

5-
internal static class DeepClonerCache
5+
internal static class FastClonerCache
66
{
77
private static readonly ConcurrentDictionary<Type, object> _typeCache = new ConcurrentDictionary<Type, object>();
88

0 commit comments

Comments
 (0)