Skip to content

Commit ff5256b

Browse files
committed
Refactor and enhance ObjectCloneHelper class for improved robustness and readability
1 parent d5ae70f commit ff5256b

File tree

4 files changed

+310
-133
lines changed

4 files changed

+310
-133
lines changed

SharpHelpers/SharpHelpers.UnitTest/SharpHelpers.UnitTest.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
<ItemGroup>
1010
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
11-
<PackageReference Include="MSTest.TestAdapter" Version="3.6.3" />
12-
<PackageReference Include="MSTest.TestFramework" Version="3.6.3" />
11+
<PackageReference Include="MSTest.TestAdapter" Version="3.6.4" />
12+
<PackageReference Include="MSTest.TestFramework" Version="3.6.4" />
1313
</ItemGroup>
1414

1515
<ItemGroup>

SharpHelpers/SharpHelpers/ObjectExtensions/ObjectCloneHelper.cs

Lines changed: 111 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,83 +14,146 @@ public static class ObjectCloneHelper
1414
#region Private Properties
1515

1616
private const BindingFlags Binding = BindingFlags.Instance |
17-
BindingFlags.NonPublic | BindingFlags.Public |
17+
BindingFlags.NonPublic |
18+
BindingFlags.Public |
1819
BindingFlags.FlattenHierarchy;
1920

2021
#endregion
2122

22-
public static T Clone<T>(this object istance,ICollection<string> propertyExcludeList = null)
23+
/// <summary>
24+
/// Clones an object and returns a deep copy of type T.
25+
/// Excluded properties can be specified via a list of property names.
26+
/// </summary>
27+
/// <typeparam name="T">The type of the cloned object.</typeparam>
28+
/// <param name="instance">The object to clone.</param>
29+
/// <param name="propertyExcludeList">A list of property names to exclude from cloning.</param>
30+
/// <returns>A deep copy of the object, or default(T) if the object is null.</returns>
31+
public static T Clone<T>(this object instance, ICollection<string> propertyExcludeList = null)
2332
{
24-
if (istance == null)
33+
if (instance == null)
2534
return default;
2635

27-
return (T) DeepClone(istance,propertyExcludeList);
36+
return (T)DeepClone(instance, propertyExcludeList);
2837
}
2938

30-
public static object Clone(this object istance)
39+
/// <summary>
40+
/// Clones an object and returns a deep copy.
41+
/// </summary>
42+
/// <param name="instance">The object to clone.</param>
43+
/// <returns>A deep copy of the object.</returns>
44+
public static object Clone(this object instance)
3145
{
32-
return DeepClone(istance);
46+
return DeepClone(instance);
3347
}
3448

35-
#region Privat Method Deep Clone
49+
#region Private Method: DeepClone
3650

37-
// Clone the object Properties and its children recursively
38-
private static object DeepClone(object istance,ICollection<string> propertyExcludeList = null)
51+
/// <summary>
52+
/// Recursively clones an object and its children.
53+
/// </summary>
54+
/// <param name="instance">The object to clone.</param>
55+
/// <param name="propertyExcludeList">A list of property names to exclude from cloning.</param>
56+
/// <returns>A deep copy of the object.</returns>
57+
private static object DeepClone(object instance, ICollection<string> propertyExcludeList = null)
3958
{
40-
var desireObjectToBeCloned = istance;
59+
if (instance == null)
60+
return null;
4161

42-
var primaryType = istance.GetType();
62+
var primaryType = instance.GetType();
4363

64+
// Handle arrays
4465
if (primaryType.IsArray)
45-
return ((Array) desireObjectToBeCloned).Clone();
66+
return ((Array)instance).Clone();
4667

47-
object tObject = desireObjectToBeCloned as IList;
48-
if (tObject != null)
68+
// Handle collections (IList)
69+
if (typeof(IList).IsAssignableFrom(primaryType))
4970
{
50-
var properties = primaryType.GetProperties();
51-
// Get the IList Type of the object
52-
var customList = typeof(List<>).MakeGenericType
53-
((properties[properties.Length - 1]).PropertyType);
54-
tObject = (IList) Activator.CreateInstance(customList);
55-
var list = (IList) tObject;
56-
// loop throw each object in the list and clone it
57-
foreach (var item in ((IList) desireObjectToBeCloned))
71+
var listType = typeof(List<>).MakeGenericType(primaryType.GetGenericArguments().FirstOrDefault() ?? typeof(object));
72+
var listClone = (IList)Activator.CreateInstance(listType);
73+
74+
foreach (var item in (IList)instance)
5875
{
59-
if (item == null)
60-
continue;
61-
var value = DeepClone(item,propertyExcludeList);
62-
list?.Add(value);
76+
listClone.Add(item == null ? null : DeepClone(item, propertyExcludeList));
6377
}
78+
79+
return listClone;
6480
}
65-
else
81+
82+
// Handle strings
83+
if (primaryType == typeof(string))
84+
return string.Copy((string)instance);
85+
86+
// Handle value types (primitives, structs, enums)
87+
if (primaryType.IsValueType || primaryType.IsPrimitive || primaryType.IsEnum)
88+
return instance;
89+
90+
// Handle complex objects
91+
var clonedObject = FormatterServices.GetUninitializedObject(primaryType);
92+
var fields = primaryType.GetFields(Binding);
93+
94+
foreach (var field in fields)
6695
{
67-
// if the item is a string then Clone it and return it directly.
68-
if (primaryType == typeof(string))
69-
return (desireObjectToBeCloned as string)?.Clone();
70-
71-
// Create an empty object and ignore its construtore.
72-
tObject = FormatterServices.GetUninitializedObject(primaryType);
73-
var fields = desireObjectToBeCloned.GetType().GetFields(Binding);
74-
foreach (var property in fields)
96+
// Skip excluded fields
97+
if (propertyExcludeList != null && propertyExcludeList.Any())
7598
{
76-
if((propertyExcludeList!=null) && (propertyExcludeList.Any()))
77-
if (propertyExcludeList.Contains(property.Name.ExtractBetween("<",">")?.FirstOrDefault()))
78-
continue;
79-
80-
if (property.IsInitOnly) // Validate if the property is a writable one.
99+
var fieldName = field.Name.ExtractBetween("<", ">")?.FirstOrDefault() ?? field.Name;
100+
if (propertyExcludeList.Contains(fieldName))
81101
continue;
82-
var value = property.GetValue(desireObjectToBeCloned);
83-
if (property.FieldType.IsClass && property.FieldType != typeof(string))
84-
tObject.GetType().GetField(property.Name, Binding)?.SetValue
85-
(tObject, DeepClone(value,propertyExcludeList));
86-
else
87-
tObject.GetType().GetField(property.Name, Binding)?.SetValue(tObject, value);
88102
}
103+
104+
// Skip readonly fields
105+
if (field.IsInitOnly)
106+
continue;
107+
108+
var value = field.GetValue(instance);
109+
110+
// Clone child objects if they are classes (except strings)
111+
var clonedValue = field.FieldType.IsClass && field.FieldType != typeof(string)
112+
? DeepClone(value, propertyExcludeList)
113+
: value;
114+
115+
field.SetValue(clonedObject, clonedValue);
89116
}
90117

91-
return tObject;
118+
return clonedObject;
92119
}
93120

94121
#endregion
95122
}
96-
}
123+
124+
/// <summary>
125+
/// Helper extensions for string operations.
126+
/// </summary>
127+
public static class StringExtensions
128+
{
129+
/// <summary>
130+
/// Extracts a substring between two delimiters.
131+
/// </summary>
132+
/// <param name="input">The input string.</param>
133+
/// <param name="startDelimiter">The starting delimiter.</param>
134+
/// <param name="endDelimiter">The ending delimiter.</param>
135+
/// <returns>An enumerable of substrings found between the delimiters.</returns>
136+
public static IEnumerable<string> ExtractBetween(this string input, string startDelimiter, string endDelimiter)
137+
{
138+
if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(startDelimiter) || string.IsNullOrEmpty(endDelimiter))
139+
return Enumerable.Empty<string>();
140+
141+
var results = new List<string>();
142+
var startIndex = 0;
143+
144+
while ((startIndex = input.IndexOf(startDelimiter, startIndex)) != -1)
145+
{
146+
startIndex += startDelimiter.Length;
147+
var endIndex = input.IndexOf(endDelimiter, startIndex);
148+
149+
if (endIndex == -1)
150+
break;
151+
152+
results.Add(input[startIndex..endIndex]);
153+
startIndex = endIndex + endDelimiter.Length;
154+
}
155+
156+
return results;
157+
}
158+
}
159+
}

0 commit comments

Comments
 (0)