diff --git a/PowerKit.Tests/Extensions/TaskExtensionsTests.cs b/PowerKit.Tests/Extensions/TaskExtensionsTests.cs new file mode 100644 index 0000000..01a382c --- /dev/null +++ b/PowerKit.Tests/Extensions/TaskExtensionsTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using PowerKit.Extensions; +using Xunit; + +namespace PowerKit.Tests.Extensions; + +public class TaskExtensionsTests +{ + [Fact] + public async Task ObserveException_ReturnsFaultException_Test() + { + // Arrange + var task = Task.Run(() => throw new InvalidOperationException("test error")); + + // Act + var exception = await task.ObserveException(); + + // Assert + task.IsFaulted.Should().BeTrue(); + exception.Should().NotBeNull(); + exception!.InnerException.Should().BeOfType(); + } + + [Fact] + public async Task ObserveException_SuccessfulTask_ReturnsNull_Test() + { + // Arrange + var task = Task.CompletedTask; + + // Act + var exception = await task.ObserveException(); + + // Assert + task.IsCompletedSuccessfully.Should().BeTrue(); + exception.Should().BeNull(); + } + + [Fact] + public async Task ObserveException_DoesNotRaiseUnobservedTaskException_Test() + { + // Arrange + var unobservedRaised = false; + + EventHandler handler = (_, e) => + { + if (e.Exception.InnerException is InvalidOperationException { Message: "test error" }) + unobservedRaised = true; + + e.SetObserved(); + }; + + TaskScheduler.UnobservedTaskException += handler; + + try + { + // Act + _ = Task.Run(() => throw new InvalidOperationException("test error")) + .ObserveException(); + + await Task.Delay(50); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + // Assert + unobservedRaised.Should().BeFalse(); + } + finally + { + TaskScheduler.UnobservedTaskException -= handler; + } + } +} diff --git a/PowerKit/Extensions/TaskExtensions.cs b/PowerKit/Extensions/TaskExtensions.cs new file mode 100644 index 0000000..7e0cf36 --- /dev/null +++ b/PowerKit/Extensions/TaskExtensions.cs @@ -0,0 +1,30 @@ +#if !NETFRAMEWORK || NET45_OR_GREATER +using System; +using System.Threading.Tasks; + +namespace PowerKit.Extensions; + +/// +/// Extensions for . +/// +public static class TaskExtensions +{ + extension(Task task) + { + /// + /// Registers a continuation that observes and suppresses the task's exception, + /// preventing it from surfacing as an unobserved task exception. + /// Returns a that resolves to the observed + /// , or if the task did not fault. + /// Intended for use on detached (fire-and-forget) tasks. + /// + public Task ObserveException() => + task.ContinueWith( + static t => t.Exception, + default, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default + ); + } +} +#endif