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
68 changes: 68 additions & 0 deletions DeepClone.Test/EnumerableCloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,72 @@ public void Stack_DeepClone_NullSource_ShouldThrow()
Stack<SimpleObject> source = null!;
Assert.ThrowsExactly<ArgumentNullException>(() => source.DeepClone());
}

/// <summary>
/// Tests that cloning from a read-only dictionary into a dictionary clones the values, rather than
/// copying the pairs and sharing the value instances with the source (ktsu-dev/DeepClone#82).
/// </summary>
[TestMethod]
public void DeepCloneFrom_ReadOnlyDictionarySource_ShouldCloneKeysAndValues()
{
// Arrange
Dictionary<SimpleObject, SimpleObject> original = new()
{
[new() { Id = 1, Name = "Key" }] = new() { Id = 2, Name = "Value" },
};
Dictionary<SimpleObject, SimpleObject> dest = [];

// Act
dest.DeepCloneFrom((IReadOnlyDictionary<SimpleObject, SimpleObject>)original);

// Assert
KeyValuePair<SimpleObject, SimpleObject> source = original.Single();
KeyValuePair<SimpleObject, SimpleObject> clone = dest.Single();
Assert.AreNotSame(source.Key, clone.Key);
Assert.AreNotSame(source.Value, clone.Value);
Assert.AreEqual("Key", clone.Key.Name);
Assert.AreEqual("Value", clone.Value.Name);
}

/// <summary>
/// Tests that deep cloning a LINQ query over a dictionary clones each pair's key and value
/// (ktsu-dev/DeepClone#82).
/// </summary>
[TestMethod]
public void Enumerable_DeepClone_LinqOverDictionary_ShouldCloneKeysAndValues()
{
// Arrange
Dictionary<string, SimpleObject> original = new()
{
["a"] = new() { Id = 1, Name = "Item1" },
["b"] = new() { Id = 2, Name = "Item2" },
};

// Act
List<KeyValuePair<string, SimpleObject>> clone = [.. original.Where(_ => true).DeepClone()];

// Assert
Assert.HasCount(2, clone);
foreach (KeyValuePair<string, SimpleObject> pair in clone)
{
Assert.AreNotSame(original[pair.Key], pair.Value);
Assert.AreEqual(original[pair.Key].Name, pair.Value.Name);
}
}

/// <summary>
/// Tests that a pair whose key and value are not cloneable is copied through unchanged.
/// </summary>
[TestMethod]
public void Enumerable_DeepClone_PairsOfNonCloneables_ShouldKeepTheirContents()
{
// Arrange
KeyValuePair<string, int>[] original = [new("a", 1), new("b", 2)];

// Act
KeyValuePair<string, int>[] clone = [.. original.AsEnumerable().DeepClone()];

// Assert
Assert.AreSequenceEqual(original, clone);
}
}
55 changes: 54 additions & 1 deletion DeepClone/DeepCloneContainerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,61 @@
/// This internal method is used by all other extension methods to handle deep cloning
/// of individual elements. It checks if the object implements IDeepCloneable and calls
/// DeepClone() if it does, otherwise it returns the original object.
///
/// A <see cref="KeyValuePair{TKey, TValue}"/> is rebuilt from a deep clone of its key and value.
/// Without that, a dictionary reached only as a sequence of pairs (an <see cref="IReadOnlyDictionary{TKey, TValue}"/>
/// passed to <see cref="DeepCloneFrom{T}(ICollection{T}, IEnumerable{T})"/>, or LINQ over a dictionary)
/// came back sharing its keys and values with the source (ktsu-dev/DeepClone#82).
/// </remarks>
private static T DeepClone<T>(T source) => source == null ? default! : source is IDeepCloneable cloneable ? (T)cloneable.DeepClone() : source;
private static T DeepClone<T>(T source) => source switch
{
null => default!,
IDeepCloneable cloneable => (T)cloneable.DeepClone(),
_ when PairCloner<T>.Instance is { } pairCloner => pairCloner.Clone(source),
_ => source,
};

/// <summary>
/// Deep clones an element of a particular type.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
private interface IElementCloner<T>
{
/// <summary>
/// Deep clones <paramref name="source"/>.
/// </summary>
/// <param name="source">The element to clone.</param>
/// <returns>The clone.</returns>
public T Clone(T source);
}

/// <summary>
/// Deep clones the key and value of a key-value pair.
/// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <typeparam name="TValue">The type of the value.</typeparam>
private sealed class PairElementCloner<TKey, TValue> : IElementCloner<KeyValuePair<TKey, TValue>>
{
/// <inheritdoc />
public KeyValuePair<TKey, TValue> Clone(KeyValuePair<TKey, TValue> source) =>
new(DeepClone(source.Key), DeepClone(source.Value));
}

/// <summary>
/// Caches, per element type, the cloner that clones it as a key-value pair.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
private static class PairCloner<T>
{
/// <summary>
/// Gets a <see cref="PairElementCloner{TKey, TValue}"/> for <typeparamref name="T"/> when it is a
/// <see cref="KeyValuePair{TKey, TValue}"/>, otherwise <see langword="null"/>.
/// </summary>
internal static IElementCloner<T>? Instance { get; } =
typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(KeyValuePair<,>)
? (IElementCloner<T>)Activator.CreateInstance(typeof(PairElementCloner<,>).MakeGenericType(typeof(T).GetGenericArguments()))!
: null;
}

/// <summary>
/// Deep clones a collection of objects.
Expand All @@ -118,7 +171,7 @@
/// </code>
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if source is null.</exception>
public static IEnumerable<T> DeepClone<T>(this IEnumerable<T> source)

Check warning on line 174 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 174 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 174 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 174 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 174 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 174 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 174 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 174 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 174 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 174 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

All 'DeepClone' method overloads should be adjacent.
{
Ensure.NotNull(source);

Expand Down
Loading