diff --git a/X10D.Tests/src/Collections/CollectionTests.cs b/X10D.Tests/src/Collections/CollectionTests.cs new file mode 100644 index 0000000..850ec06 --- /dev/null +++ b/X10D.Tests/src/Collections/CollectionTests.cs @@ -0,0 +1,76 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Collections; + +namespace X10D.Tests.Collections; + +[TestClass] +public class CollectionTests +{ + [TestMethod] + public void ClearAndDisposeAll_ShouldDispose_GivenCollection() + { + var collection = new List {new(), new(), new()}; + var copy = new List(collection); + + collection.ClearAndDisposeAll(); + + Assert.IsTrue(copy.All(x => x.IsDisposed)); + Assert.AreEqual(0, collection.Count); + } + + [TestMethod] + public async Task ClearAndDisposeAllAsync_ShouldDispose_GivenCollection() + { + var collection = new List {new(), new(), new()}; + var copy = new List(collection); + + await collection.ClearAndDisposeAllAsync(); + + Assert.IsTrue(copy.All(x => x.IsDisposed)); + Assert.AreEqual(0, collection.Count); + } + + [TestMethod] + public void ClearAndDisposeAll_ShouldThrow_GivenNull() + { + List? collection = null; + Assert.ThrowsException(() => collection!.ClearAndDisposeAll()); + } + + [TestMethod] + public void ClearAndDisposeAllAsync_ShouldThrow_GivenNull() + { + List? collection = null; + Assert.ThrowsExceptionAsync(async () => await collection!.ClearAndDisposeAllAsync()); + } + + [TestMethod] + public void ClearAndDisposeAll_ShouldThrow_GivenReadOnlyCollection() + { + var collection = new List().AsReadOnly(); + Assert.ThrowsException(() => collection.ClearAndDisposeAll()); + } + + [TestMethod] + public void ClearAndDisposeAllAsync_ShouldThrow_GivenReadOnlyCollection() + { + var collection = new List().AsReadOnly(); + Assert.ThrowsExceptionAsync(async () => await collection.ClearAndDisposeAllAsync()); + } + + private class Disposable : IDisposable, IAsyncDisposable + { + public bool IsDisposed { get; private set; } + + public void Dispose() + { + Assert.IsTrue(IsDisposed = true); + } + + public ValueTask DisposeAsync() + { + Assert.IsTrue(IsDisposed = true); + return ValueTask.CompletedTask; + } + } +} diff --git a/X10D.Tests/src/Collections/EnumerableTests.cs b/X10D.Tests/src/Collections/EnumerableTests.cs index 4e95f49..851277f 100644 --- a/X10D.Tests/src/Collections/EnumerableTests.cs +++ b/X10D.Tests/src/Collections/EnumerableTests.cs @@ -1,4 +1,4 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using X10D.Collections; namespace X10D.Tests.Collections; @@ -6,6 +6,36 @@ namespace X10D.Tests.Collections; [TestClass] public class EnumerableTests { + [TestMethod] + public void DisposeAll_ShouldDispose_GivenCollection() + { + var collection = new List {new(), new(), new()}; + collection.DisposeAll(); + Assert.IsTrue(collection.All(x => x.IsDisposed)); + } + + [TestMethod] + public async Task DisposeAllAsync_ShouldDispose_GivenCollection() + { + var collection = new List {new(), new(), new()}; + await collection.DisposeAllAsync(); + Assert.IsTrue(collection.All(x => x.IsDisposed)); + } + + [TestMethod] + public void DisposeAll_ShouldThrow_GivenNull() + { + List? collection = null; + Assert.ThrowsException(() => collection!.DisposeAll()); + } + + [TestMethod] + public async Task DisposeAllAsync_ShouldThrow_GivenNull() + { + List? collection = null; + await Assert.ThrowsExceptionAsync(async () => await collection!.DisposeAllAsync()); + } + [TestMethod] public void For_ShouldTransform_GivenTransformationDelegate() { @@ -86,4 +116,20 @@ public class EnumerableTests { public int Value { get; set; } } + + private class Disposable : IDisposable, IAsyncDisposable + { + public bool IsDisposed { get; private set; } + + public void Dispose() + { + Assert.IsTrue(IsDisposed = true); + } + + public ValueTask DisposeAsync() + { + Assert.IsTrue(IsDisposed = true); + return ValueTask.CompletedTask; + } + } } diff --git a/X10D/src/Collections/CollectionExtensions.cs b/X10D/src/Collections/CollectionExtensions.cs new file mode 100644 index 0000000..6bad398 --- /dev/null +++ b/X10D/src/Collections/CollectionExtensions.cs @@ -0,0 +1,77 @@ +namespace X10D.Collections; + +/// +/// Collection-related extension methods for . +/// +public static class CollectionExtensions +{ + /// + /// Calls on each item in the collection, then clears the collection by calling + /// . + /// + /// The collection to clear, and whose elements should be disposed. + /// The type of the elements in . + /// is . + /// is read-only. + /// + public static void ClearAndDisposeAll(this ICollection source) where T : IDisposable + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (source.IsReadOnly) + { + throw new InvalidOperationException("Collection is read-only. Try using DisposeAll instead."); + } + + foreach (T item in source) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (item is null) + { + continue; + } + + item.Dispose(); + } + + source.Clear(); + } + + /// + /// Asynchronously calls on each item in the collection, then clears the + /// collection by calling . + /// + /// The collection to clear, and whose elements should be disposed. + /// The type of the elements in . + /// is . + /// is read-only. + /// + public static async Task ClearAndDisposeAllAsync(this ICollection source) where T : IAsyncDisposable + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (source.IsReadOnly) + { + throw new InvalidOperationException("Collection is read-only. Try using DisposeAllAsync instead."); + } + + foreach (T item in source) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (item is null) + { + continue; + } + + await item.DisposeAsync(); + } + + source.Clear(); + } +} diff --git a/X10D/src/Collections/EnumerableExtensions.cs b/X10D/src/Collections/EnumerableExtensions.cs index 2dfc589..46e9126 100644 --- a/X10D/src/Collections/EnumerableExtensions.cs +++ b/X10D/src/Collections/EnumerableExtensions.cs @@ -75,6 +75,59 @@ public static class EnumerableExtensions } } + /// + /// Calls on all elements of the . + /// + /// The enumerable collection whose elements to dispose. + /// The type of the elements in . + /// is . + /// + public static void DisposeAll(this IEnumerable source) where T : IDisposable + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + foreach (T item in source) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (item is null) + { + continue; + } + + item.Dispose(); + } + } + + /// + /// Asynchronously calls on all elements of the + /// . + /// + /// The enumerable collection whose elements to dispose. + /// The type of the elements in . + /// is . + /// + public static async Task DisposeAllAsync(this IEnumerable source) where T : IAsyncDisposable + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + foreach (T item in source) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (item is null) + { + continue; + } + + await item.DisposeAsync(); + } + } + /// /// Reorganizes the elements in an enumerable by implementing a Fisher-Yates shuffle, and returns th shuffled result. ///