Skip to content
Merged
76 changes: 76 additions & 0 deletions PowerKit.Tests/Extensions/TaskExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -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<InvalidOperationException>();
}

[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<UnobservedTaskExceptionEventArgs> 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;
}
}
}
30 changes: 30 additions & 0 deletions PowerKit/Extensions/TaskExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#if !NETFRAMEWORK || NET45_OR_GREATER
using System;
using System.Threading.Tasks;

namespace PowerKit.Extensions;

/// <summary>
/// Extensions for <see cref="Task" />.
/// </summary>
public static class TaskExtensions
{
extension(Task task)
{
/// <summary>
/// Registers a continuation that observes and suppresses the task's exception,
/// preventing it from surfacing as an unobserved task exception.
/// Returns a <see cref="Task{TResult}" /> that resolves to the observed
/// <see cref="AggregateException" />, or <see langword="null" /> if the task did not fault.
/// Intended for use on detached (fire-and-forget) tasks.
/// </summary>
public Task<AggregateException?> ObserveException() =>
task.ContinueWith(
static t => t.Exception,
default,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default
);
}
}
#endif