diff --git a/DeepClone.Test/DictionaryComparerTests.cs b/DeepClone.Test/DictionaryComparerTests.cs
index 0c1f2f1..a9046d8 100644
--- a/DeepClone.Test/DictionaryComparerTests.cs
+++ b/DeepClone.Test/DictionaryComparerTests.cs
@@ -4,6 +4,7 @@ namespace ktsu.DeepClone.Test;
using System.Collections.Concurrent;
using System.Collections.Immutable;
+using System.Collections.ObjectModel;
///
/// Tests that cloning a dictionary through or
@@ -122,4 +123,82 @@ public void ImmutableSortedDictionary_ThroughIDictionary_KeepsComparerAndOrder()
CollectionAssert.AreEqual(DescendingKeys, clone.Keys.ToArray());
}
+
+ ///
+ /// Tests that a read-only dictionary over a case-insensitive dictionary keeps the wrapped dictionary's comparer
+ /// when cloned through its concrete overload (ktsu-dev/DeepClone#87).
+ ///
+ [TestMethod]
+ public void ReadOnlyDictionary_KeepsWrappedComparer()
+ {
+ ReadOnlyDictionary original = new(new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Key"] = 1 });
+
+ ReadOnlyDictionary clone = original.DeepClone();
+
+ Assert.IsTrue(clone.ContainsKey("KEY"));
+ }
+
+ ///
+ /// Tests that a read-only dictionary over a case-insensitive dictionary keeps the wrapped dictionary's comparer
+ /// when cloned through IReadOnlyDictionary.
+ ///
+ [TestMethod]
+ public void ReadOnlyDictionary_ThroughIReadOnlyDictionary_KeepsWrappedComparer()
+ {
+ ReadOnlyDictionary original = new(new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Key"] = 1 });
+
+ IReadOnlyDictionary clone = ((IReadOnlyDictionary)original).DeepClone();
+
+ Assert.IsTrue(clone.ContainsKey("KEY"));
+ }
+
+ ///
+ /// Tests that a read-only dictionary cloned through IDictionary keeps the wrapped dictionary's comparer.
+ ///
+ [TestMethod]
+ public void ReadOnlyDictionary_ThroughIDictionary_KeepsWrappedComparer()
+ {
+ ReadOnlyDictionary original = new(new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Key"] = 1 });
+
+ IDictionary clone = ((IDictionary)original).DeepClone();
+
+ Assert.IsTrue(clone.ContainsKey("KEY"));
+ }
+
+ ///
+ /// Tests that a read-only dictionary over a reference-equality dictionary holding two equal but distinct keys
+ /// clones without throwing and keeps both entries.
+ ///
+ [TestMethod]
+ public void ReadOnlyDictionary_WithReferenceEquality_ClonesEqualKeys()
+ {
+ Dictionary
/// The type of keys in the dictionary.
/// The type of values in the dictionary.
- /// The dictionary being cloned, inspected for its runtime type and comparer.
+ /// The dictionary being cloned, inspected for its runtime type and comparer. A
+ /// is looked through to the dictionary it wraps.
/// The key-value pairs of the dictionary being cloned.
/// A or for a source of
/// that type, a for any other sorted source, otherwise a
@@ -349,6 +354,11 @@ public static IReadOnlyDictionary DeepClone(this IRe
private static IDictionary CloneDictionary(object source, IEnumerable> pairs)
where TKey : notnull
{
+ while (source is ReadOnlyDictionary readOnly)
+ {
+ source = WrappedDictionary.Of(readOnly);
+ }
+
IDictionary clone = source switch
{
SortedDictionary sorted => new SortedDictionary(sorted.Comparer),
@@ -368,6 +378,33 @@ private static IDictionary CloneDictionary(object so
return clone;
}
+ ///
+ /// Reads the dictionary a wraps, which it exposes only through
+ /// its protected Dictionary property, so a clone can keep that dictionary's kind and comparer.
+ ///
+ /// The type of keys in the dictionary.
+ /// The type of values in the dictionary.
+ private static class WrappedDictionary
+ where TKey : notnull
+ {
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Major Code Smell",
+ "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields",
+ Justification = "ReadOnlyDictionary exposes the dictionary it wraps only through a protected property, and the getter is only read, never used to modify it. Without it the clone cannot keep the wrapped dictionary's comparer (ktsu-dev/DeepClone#87).")]
+ private static readonly Func, IDictionary> Getter =
+ typeof(ReadOnlyDictionary)
+ .GetProperty("Dictionary", BindingFlags.Instance | BindingFlags.NonPublic)!
+ .GetGetMethod(nonPublic: true)!
+ .CreateDelegate, IDictionary>>();
+
+ ///
+ /// Gets the dictionary that wraps.
+ ///
+ /// The read-only wrapper.
+ /// The wrapped dictionary.
+ internal static IDictionary Of(ReadOnlyDictionary readOnly) => Getter(readOnly);
+ }
+
///
/// Adds a deep clone of each key-value pair to a destination dictionary.
///