diff --git a/PowerKit.Tests/AsyncDisposableExtensionsTests.cs b/PowerKit.Tests/AsyncDisposableExtensionsTests.cs new file mode 100644 index 0000000..65a2646 --- /dev/null +++ b/PowerKit.Tests/AsyncDisposableExtensionsTests.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using PowerKit.Extensions; +using Xunit; + +namespace PowerKit.Tests; + +file class AsyncDisposableStub(Action onDispose) : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new Exception("DisposeAsync() should've been called instead"); + + public ValueTask DisposeAsync() + { + onDispose(); + return default; + } +} + +file class DisposableStub(Action onDispose) : IDisposable +{ + public void Dispose() => onDispose(); +} + +public class AsyncDisposableExtensionsTests +{ + [Fact] + public async Task ToAsyncDisposable_IAsyncDisposable_Test() + { + // Arrange + var asyncDisposeCalled = false; + + var disposable = new AsyncDisposableStub(() => asyncDisposeCalled = true); + + // Act + await disposable.ToAsyncDisposable().DisposeAsync(); + + // Assert + asyncDisposeCalled.Should().BeTrue(); + } + + [Fact] + public async Task ToAsyncDisposable_IDisposable_Test() + { + // Arrange + var disposeCalled = false; + + var disposable = new DisposableStub(() => disposeCalled = true); + + // Act + await disposable.ToAsyncDisposable().DisposeAsync(); + + // Assert + disposeCalled.Should().BeTrue(); + } +} diff --git a/PowerKit/Extensions/AsyncDisposableExtensions.cs b/PowerKit/Extensions/AsyncDisposableExtensions.cs new file mode 100644 index 0000000..12bb9f5 --- /dev/null +++ b/PowerKit/Extensions/AsyncDisposableExtensions.cs @@ -0,0 +1,47 @@ +#if NET40_OR_GREATER || NETSTANDARD || NET +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace PowerKit.Extensions; + +// Provides a dynamic and uniform way to deal with async disposable. +// Used as an abstraction to dynamically polyfill IAsyncDisposable implementations in BCL types. For example: +// - Stream class on .NET Framework 4.6.1 -> calls Dispose() +// - Stream class on .NET Core 3.0 -> calls DisposeAsync() +// - Stream class on .NET Standard 2.0 -> calls DisposeAsync() or Dispose(), depending on the runtime +file class AsyncDisposableAdapter(IDisposable target) : IAsyncDisposable +{ + public async ValueTask DisposeAsync() + { + if (target is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + } + else + { + target.Dispose(); + } + } +} + +#if !POWERKIT_INCLUDE_COVERAGE +[ExcludeFromCodeCoverage] +#endif +internal static class AsyncDisposableExtensions +{ + extension(IDisposable disposable) + { + /// + /// Wraps the disposable in an adapter that calls + /// if supported, or falls back to + /// . + /// + public IAsyncDisposable ToAsyncDisposable() => + disposable is IAsyncDisposable asyncDisposable + ? asyncDisposable + : new AsyncDisposableAdapter(disposable); + } +} +#endif