diff --git a/DeepClone.Test/DeepCloneTests.cs b/DeepClone.Test/DeepCloneTests.cs index b5d2267..cfafb88 100644 --- a/DeepClone.Test/DeepCloneTests.cs +++ b/DeepClone.Test/DeepCloneTests.cs @@ -314,8 +314,7 @@ public void ContainerExtensions_DeepClone_ShouldCreateIndependentCopies() // Act // Use IEnumerable for list to avoid ambiguity List? clonedList = originalList.DeepClone()?.ToList(); - // Cast to IDictionary to resolve ambiguity - IDictionary clonedDict = ((IDictionary)originalDict).DeepClone(); + Dictionary clonedDict = originalDict.DeepClone(); // Use nullable types for HashSet and Stack HashSet clonedSet = originalSet.DeepClone(); Stack clonedStack = originalStack.DeepClone(); diff --git a/DeepClone.Test/SpecializedCollectionTests.cs b/DeepClone.Test/SpecializedCollectionTests.cs index 6679852..1243718 100644 --- a/DeepClone.Test/SpecializedCollectionTests.cs +++ b/DeepClone.Test/SpecializedCollectionTests.cs @@ -3,6 +3,7 @@ namespace ktsu.DeepClone.Test; using System.Collections.Concurrent; +using System.Collections.ObjectModel; /// /// Tests for specialized collection types not covered by other tests. @@ -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 originalDict = original; - IDictionary clone = DeepCloneContainerExtensions.DeepClone(originalDict); + // Act + ConcurrentDictionary clone = original.DeepClone(); // Assert Assert.IsNotNull(clone); @@ -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"); } + + /// + /// Tests that deep cloning a ConcurrentDictionary keeps its key comparer. + /// + [TestMethod] + public void ConcurrentDictionary_DeepClone_ShouldKeepComparer() + { + // Arrange + ConcurrentDictionary original = new(StringComparer.OrdinalIgnoreCase); + original.TryAdd("Key", new SimpleObject { Id = 1, Name = "Item1" }); + + // Act + ConcurrentDictionary 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"]); + } + + /// + /// Tests deep cloning a ReadOnlyDictionary. + /// + [TestMethod] + public void ReadOnlyDictionaryWrapper_DeepClone_ShouldCreateIndependentCopy() + { + // Arrange + Dictionary inner = new(StringComparer.OrdinalIgnoreCase) + { + ["Key"] = new SimpleObject { Id = 1, Name = "Item1" }, + }; + ReadOnlyDictionary original = new(inner); + + // Act + ReadOnlyDictionary 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 diff --git a/DeepClone/DeepCloneContainerExtensions.cs b/DeepClone/DeepCloneContainerExtensions.cs index 11d5f80..59afd2d 100644 --- a/DeepClone/DeepCloneContainerExtensions.cs +++ b/DeepClone/DeepCloneContainerExtensions.cs @@ -2,6 +2,9 @@ namespace ktsu.DeepClone; +using System.Collections.Concurrent; +using System.Collections.ObjectModel; + /// /// Extension methods for deep cloning collections of objects. /// @@ -187,6 +190,56 @@ public static SortedDictionary DeepClone(this Sorted return clone; } + /// + /// Deep clones a concurrent dictionary, keeping its key comparer where the target framework exposes it. + /// + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + /// The source dictionary to clone. + /// A new concurrent dictionary containing deep clones of the keys and values + /// if they implement IDeepCloneable, otherwise containing the original keys and values. + /// + /// implements both and + /// , so this overload is also what lets + /// concurrentDictionary.DeepClone() 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. + /// + /// Thrown if source is null. + public static ConcurrentDictionary DeepClone(this ConcurrentDictionary source) + where TKey : notnull + { + Ensure.NotNull(source); + + IEnumerable> pairs = source.Select(p => new KeyValuePair(DeepClone(p.Key), DeepClone(p.Value))); +#if NET6_0_OR_GREATER + return new(pairs, source.Comparer); +#else + return new(pairs); +#endif + } + + /// + /// Deep clones a read-only dictionary wrapper. + /// + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + /// The source dictionary to clone. + /// 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. + /// + /// implements both and + /// , so this overload is also what lets + /// readOnlyDictionary.DeepClone() compile without a cast. + /// + /// Thrown if source is null. + public static ReadOnlyDictionary DeepClone(this ReadOnlyDictionary source) + where TKey : notnull + { + Ensure.NotNull(source); + + return new(CloneDictionary(source, source)); + } + /// /// Deep clones a read-only dictionary. ///