Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions DeepClone.Test/DeepCloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,7 @@ public void ContainerExtensions_DeepClone_ShouldCreateIndependentCopies()
// Act
// Use IEnumerable for list to avoid ambiguity
List<SimpleObject?>? clonedList = originalList.DeepClone()?.ToList();
// Cast to IDictionary to resolve ambiguity
IDictionary<string, SimpleObject?> clonedDict = ((IDictionary<string, SimpleObject?>)originalDict).DeepClone();
Dictionary<string, SimpleObject?> clonedDict = originalDict.DeepClone();
// Use nullable types for HashSet and Stack
HashSet<SimpleObject?> clonedSet = originalSet.DeepClone();
Stack<SimpleObject?> clonedStack = originalStack.DeepClone();
Expand Down
54 changes: 50 additions & 4 deletions DeepClone.Test/SpecializedCollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace ktsu.DeepClone.Test;

using System.Collections.Concurrent;
using System.Collections.ObjectModel;

/// <summary>
/// Tests for specialized collection types not covered by other tests.
Expand Down Expand Up @@ -348,9 +349,8 @@ public void ConcurrentDictionary_DeepClone_ShouldCreateIndependentCopy()
original.TryAdd(1, new SimpleObject { Id = 1, Name = "Item1" });
original.TryAdd(2, new SimpleObject { Id = 2, Name = "Item2" });

// Act - explicit cast to IDictionary to resolve ambiguity
IDictionary<int, SimpleObject> originalDict = original;
IDictionary<int, SimpleObject> clone = DeepCloneContainerExtensions.DeepClone(originalDict);
// Act
ConcurrentDictionary<int, SimpleObject> clone = original.DeepClone();

// Assert
Assert.IsNotNull(clone);
Expand All @@ -372,11 +372,57 @@ public void ConcurrentDictionary_DeepClone_ShouldCreateIndependentCopy()
Assert.AreEqual("Item1", original[1].Name);

// Verify adding to clone doesn't affect original
clone.Add(3, new SimpleObject { Id = 3, Name = "Item3" });
Assert.IsTrue(clone.TryAdd(3, new SimpleObject { Id = 3, Name = "Item3" }));
Assert.HasCount(3, clone);
Assert.HasCount(2, original);
Assert.IsFalse(original.ContainsKey(3), "Original should not contain key 3 added to clone");
}

/// <summary>
/// Tests that deep cloning a ConcurrentDictionary keeps its key comparer.
/// </summary>
[TestMethod]
public void ConcurrentDictionary_DeepClone_ShouldKeepComparer()
{
// Arrange
ConcurrentDictionary<string, SimpleObject> original = new(StringComparer.OrdinalIgnoreCase);
original.TryAdd("Key", new SimpleObject { Id = 1, Name = "Item1" });

// Act
ConcurrentDictionary<string, SimpleObject> clone = original.DeepClone();

// Assert
Assert.AreSame(StringComparer.OrdinalIgnoreCase, clone.Comparer);
Assert.IsTrue(clone.ContainsKey("KEY"), "Clone should look up keys case-insensitively like the source");
Assert.AreNotSame(original["Key"], clone["Key"]);
}

/// <summary>
/// Tests deep cloning a ReadOnlyDictionary.
/// </summary>
[TestMethod]
public void ReadOnlyDictionaryWrapper_DeepClone_ShouldCreateIndependentCopy()
{
// Arrange
Dictionary<string, SimpleObject> inner = new(StringComparer.OrdinalIgnoreCase)
{
["Key"] = new SimpleObject { Id = 1, Name = "Item1" },
};
ReadOnlyDictionary<string, SimpleObject> original = new(inner);

// Act
ReadOnlyDictionary<string, SimpleObject> clone = original.DeepClone();

// Assert
Assert.HasCount(1, clone);
Assert.AreNotSame(original["Key"], clone["Key"]);
Assert.AreEqual(1, clone["Key"].Id);
Assert.AreEqual("Item1", clone["Key"].Name);

// Verify independence - the clone does not follow later changes to the wrapped dictionary
inner["Other"] = new SimpleObject { Id = 2, Name = "Item2" };
Assert.HasCount(1, clone);
}
}

#pragma warning restore IDE0007
Expand Down
53 changes: 53 additions & 0 deletions DeepClone/DeepCloneContainerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace ktsu.DeepClone;

using System.Collections.Concurrent;
using System.Collections.ObjectModel;

/// <summary>
/// Extension methods for deep cloning collections of objects.
/// </summary>
Expand Down Expand Up @@ -94,7 +97,7 @@
/// of individual elements. It checks if the object implements IDeepCloneable and calls
/// DeepClone() if it does, otherwise it returns the original object.
/// </remarks>
private static T DeepClone<T>(T source) => source == null ? default! : source is IDeepCloneable cloneable ? (T)cloneable.DeepClone() : source;

Check warning on line 100 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 100 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 100 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Use a comparison to 'default(T)' instead or add a constraint to 'T' so that it can't be a value type.

Check warning on line 100 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Use a comparison to 'default(T)' instead or add a constraint to 'T' so that it can't be a value type.

Check warning on line 100 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 100 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <summary>
/// Deep clones a collection of objects.
Expand All @@ -113,7 +116,7 @@
/// var clonedArray = originalArray.DeepClone().ToArray();
/// </code>
/// </remarks>
public static IEnumerable<T> DeepClone<T>(this IEnumerable<T> source) =>

Check warning on line 119 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

All 'DeepClone' method overloads should be adjacent.

Check warning on line 119 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

All 'DeepClone' method overloads should be adjacent.

Check warning on line 119 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

All 'DeepClone' method overloads should be adjacent.

Check warning on line 119 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

All 'DeepClone' method overloads should be adjacent.
source.Select(DeepClone);

/// <summary>
Expand Down Expand Up @@ -187,6 +190,56 @@
return clone;
}

/// <summary>
/// Deep clones a concurrent dictionary, keeping its key comparer where the target framework exposes it.
/// </summary>
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
/// <param name="source">The source dictionary to clone.</param>
/// <returns>A new concurrent dictionary containing deep clones of the keys and values
/// if they implement IDeepCloneable, otherwise containing the original keys and values.</returns>
/// <remarks>
/// <see cref="ConcurrentDictionary{TKey, TValue}"/> implements both <see cref="IDictionary{TKey, TValue}"/> and
/// <see cref="IReadOnlyDictionary{TKey, TValue}"/>, so this overload is also what lets
/// <c>concurrentDictionary.DeepClone()</c> compile without a cast. The comparer is kept on .NET 6 and later;
/// the .NET Standard builds cannot read it, so the clone uses the default comparer there.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if source is null.</exception>
public static ConcurrentDictionary<TKey, TValue> DeepClone<TKey, TValue>(this ConcurrentDictionary<TKey, TValue> source)
where TKey : notnull
{
Ensure.NotNull(source);

IEnumerable<KeyValuePair<TKey, TValue>> pairs = source.Select(p => new KeyValuePair<TKey, TValue>(DeepClone(p.Key), DeepClone(p.Value)));
#if NET6_0_OR_GREATER
return new(pairs, source.Comparer);
#else
return new(pairs);
#endif
}

/// <summary>
/// Deep clones a read-only dictionary wrapper.
/// </summary>
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
/// <param name="source">The source dictionary to clone.</param>
/// <returns>A new read-only dictionary wrapping a new dictionary that contains deep clones of the keys and values
/// if they implement IDeepCloneable, otherwise containing the original keys and values.</returns>
/// <remarks>
/// <see cref="ReadOnlyDictionary{TKey, TValue}"/> implements both <see cref="IDictionary{TKey, TValue}"/> and
/// <see cref="IReadOnlyDictionary{TKey, TValue}"/>, so this overload is also what lets
/// <c>readOnlyDictionary.DeepClone()</c> compile without a cast.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if source is null.</exception>
public static ReadOnlyDictionary<TKey, TValue> DeepClone<TKey, TValue>(this ReadOnlyDictionary<TKey, TValue> source)
where TKey : notnull
{
Ensure.NotNull(source);

return new(CloneDictionary(source, source));
}

/// <summary>
/// Deep clones a read-only dictionary.
/// </summary>
Expand Down
Loading