diff --git a/TUnit.AspNetCore.Core/FlowSuppressingHostedService.cs b/TUnit.AspNetCore.Core/FlowSuppressingHostedService.cs
index 38c2ea7101..0f8f05a9ce 100644
--- a/TUnit.AspNetCore.Core/FlowSuppressingHostedService.cs
+++ b/TUnit.AspNetCore.Core/FlowSuppressingHostedService.cs
@@ -11,12 +11,20 @@ namespace TUnit.AspNetCore;
/// as their parent.
///
///
+///
/// Implements so the Host's lifecycle hooks keep
/// firing for inner services that implement it — the Host uses an is check
/// against the registered instance, so without passthrough wrapping would silently
/// drop those hooks.
+///
+///
+/// Also implements and so the
+/// DI container forwards disposal to the inner service when the host is disposed.
+/// Without this, wrapped services that own unmanaged resources leak silently because
+/// the container only sees the non-disposable wrapper.
+///
///
-internal sealed class FlowSuppressingHostedService(IHostedService inner) : IHostedLifecycleService
+internal sealed class FlowSuppressingHostedService(IHostedService inner) : IHostedLifecycleService, IAsyncDisposable, IDisposable
{
public Task StartAsync(CancellationToken cancellationToken) =>
RunOnCleanContext(inner.StartAsync, cancellationToken);
@@ -47,6 +55,34 @@ inner is IHostedLifecycleService lifecycle
? lifecycle.StoppedAsync(cancellationToken)
: Task.CompletedTask;
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ if (inner is IAsyncDisposable asyncDisposable)
+ {
+ await asyncDisposable.DisposeAsync().ConfigureAwait(false);
+ }
+ else if (inner is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+
+ ///
+ ///
+ /// Forwards to the inner service's when implemented.
+ /// Inner services that only implement are not disposed on
+ /// this synchronous path. Callers should use (or
+ /// await using on the owning factory) to release async-only resources.
+ ///
+ public void Dispose()
+ {
+ if (inner is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+
// Dispatch onto a thread-pool worker with a clean captured ExecutionContext by
// combining SuppressFlow + Task.Run. Unlike wrapping `using (SuppressFlow()) return op(ct);`
// which only suppresses during the synchronous body, this keeps the inner operation
diff --git a/TUnit.AspNetCore.Tests/HostedServiceDisposalForwardingTests.cs b/TUnit.AspNetCore.Tests/HostedServiceDisposalForwardingTests.cs
new file mode 100644
index 0000000000..c557c7665a
--- /dev/null
+++ b/TUnit.AspNetCore.Tests/HostedServiceDisposalForwardingTests.cs
@@ -0,0 +1,145 @@
+using Microsoft.Extensions.Hosting;
+using TUnit.AspNetCore;
+
+namespace TUnit.AspNetCore.Tests;
+
+///
+/// Tests that forwards
+/// and calls to the wrapped inner service.
+/// Constructs the wrapper directly, without a host or DI container, to isolate the forwarding
+/// contract from host-disposal ordering.
+///
+public class HostedServiceDisposalForwardingTests
+{
+ [Test]
+ public async Task DisposeAsync_Forwards_To_IAsyncDisposable_Inner()
+ {
+ var inner = new AsyncDisposableProbeHostedService();
+ var wrapper = new FlowSuppressingHostedService(inner);
+
+ await ((IAsyncDisposable) wrapper).DisposeAsync();
+
+ await Assert.That(inner.DisposeAsyncCalled).IsTrue();
+ }
+
+ [Test]
+ public async Task Dispose_Forwards_To_IDisposable_Inner()
+ {
+ var inner = new DisposableProbeHostedService();
+ var wrapper = new FlowSuppressingHostedService(inner);
+
+ ((IDisposable) wrapper).Dispose();
+
+ await Assert.That(inner.DisposeCalled).IsTrue();
+ }
+
+ [Test]
+ public async Task DisposeAsync_On_SyncOnly_Inner_Falls_Back_To_Dispose()
+ {
+ var inner = new DisposableProbeHostedService();
+ var wrapper = new FlowSuppressingHostedService(inner);
+
+ await ((IAsyncDisposable) wrapper).DisposeAsync();
+
+ await Assert.That(inner.DisposeCalled).IsTrue();
+ }
+
+ [Test]
+ public async Task Dispose_On_AsyncOnly_Inner_Is_No_Op()
+ {
+ // Sync Dispose intentionally does not release an async-only inner:
+ // blocking on DisposeAsync would violate the "never block on async" rule.
+ // Callers with async-only inner services should use DisposeAsync.
+ var inner = new AsyncDisposableProbeHostedService();
+ var wrapper = new FlowSuppressingHostedService(inner);
+
+ ((IDisposable) wrapper).Dispose();
+
+ await Assert.That(inner.DisposeAsyncCalled).IsFalse();
+ }
+
+ [Test]
+ public async Task DisposeAsync_On_DualInterface_Inner_Prefers_Async_Path()
+ {
+ var inner = new DualDisposableProbeHostedService();
+ var wrapper = new FlowSuppressingHostedService(inner);
+
+ await ((IAsyncDisposable) wrapper).DisposeAsync();
+
+ await Assert.That(inner.DisposeAsyncCalled).IsTrue();
+ await Assert.That(inner.DisposeCalled).IsFalse();
+ }
+
+ [Test]
+ public async Task Dispose_On_DualInterface_Inner_Prefers_Sync_Path()
+ {
+ var inner = new DualDisposableProbeHostedService();
+ var wrapper = new FlowSuppressingHostedService(inner);
+
+ ((IDisposable) wrapper).Dispose();
+
+ await Assert.That(inner.DisposeCalled).IsTrue();
+ await Assert.That(inner.DisposeAsyncCalled).IsFalse();
+ }
+
+ [Test]
+ public async Task Wrapper_Does_Not_Throw_When_Inner_Is_Not_Disposable()
+ {
+ var inner = new NonDisposableProbeHostedService();
+ var wrapper = new FlowSuppressingHostedService(inner);
+
+ await Assert.That(() => ((IDisposable) wrapper).Dispose()).ThrowsNothing();
+ await Assert.That(async () => await ((IAsyncDisposable) wrapper).DisposeAsync()).ThrowsNothing();
+ }
+}
+
+internal sealed class DisposableProbeHostedService : IHostedService, IDisposable
+{
+ public bool DisposeCalled { get; private set; }
+
+ public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public void Dispose() => DisposeCalled = true;
+}
+
+internal sealed class AsyncDisposableProbeHostedService : IHostedService, IAsyncDisposable
+{
+ public bool DisposeAsyncCalled { get; private set; }
+
+ public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public ValueTask DisposeAsync()
+ {
+ DisposeAsyncCalled = true;
+ return ValueTask.CompletedTask;
+ }
+}
+
+internal sealed class DualDisposableProbeHostedService : IHostedService, IDisposable, IAsyncDisposable
+{
+ public bool DisposeCalled { get; private set; }
+ public bool DisposeAsyncCalled { get; private set; }
+
+ public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public void Dispose() => DisposeCalled = true;
+
+ public ValueTask DisposeAsync()
+ {
+ DisposeAsyncCalled = true;
+ return ValueTask.CompletedTask;
+ }
+}
+
+internal sealed class NonDisposableProbeHostedService : IHostedService
+{
+ public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+}