DI032

Service Implements Only IAsyncDisposable

A service the container creates — a plain type registration at any lifetime — whose implementation implements IAsyncDisposable but not IDisposable.

Warning Default severity · Code fix: No

Why it matters

The container tracks everything it creates so it can dispose it, but a synchronous Dispose() on the provider or a scope has no synchronous disposal method to call. Rather than skipping the service, ServiceProvider throws InvalidOperationException: *"'X' type only implements IAsyncDisposable. Use DisposeAsync to dispose the container."* The failure surfaces at shutdown or at the end of a scope, which is exactly where it is hardest to notice in testing.

README problem example

public sealed class UploadQueue : IUploadQueue, IAsyncDisposable
{
    public ValueTask DisposeAsync() => default;
}

services.AddSingleton<IUploadQueue, UploadQueue>();  // DI032
using var provider = services.BuildServiceProvider();  // throws on Dispose()

README better pattern

public sealed class UploadQueue : IUploadQueue, IDisposable, IAsyncDisposable
{
    public void Dispose() { }
    public ValueTask DisposeAsync() => default;
}

No — adding a synchronous Dispose means deciding what synchronous teardown of an inherently asynchronous resource should do, which the analyzer cannot answer.

Guardrails

When DI032 stays silent

The rule covers singleton and scoped registrations — a transient disposable is DI008's finding, and a second diagnostic on the same registration would be noise. Pre-built instances are exempt because the container never disposes them at all (that is DI033). Factory registrations do count — the container creates and tracks a factory's result — when the lambda body is a single object creation; an opaque factory proves nothing and stays quiet. A descriptor that is definitely removed or replaced after it was added never reaches the provider; replay is limited to the same service-collection flow and matching keyed slot, while keyed and unkeyed descriptors remain distinct. The diagnostic is conditional on the service being resolved at least once, since that is what puts it in the container's disposal list.

Repo sample extraction

Examples pulled from the sample app

Open full sample file

Sample app async-only disposable warning

    public sealed class Bad_AsyncOnlyUploadQueue : IUploadQueue, IAsyncDisposable
    {
        public Task EnqueueAsync(string payload) => Task.CompletedTask;

        public ValueTask DisposeAsync() => default;
    }

Sample app dual-disposable pattern

    public sealed class Good_DualDisposableUploadQueue : IUploadQueue, IDisposable, IAsyncDisposable
    {
        public Task EnqueueAsync(string payload) => Task.CompletedTask;

        public void Dispose() { }

        public ValueTask DisposeAsync() => default;
    }

Related guides

  • No problem-guide pages point here yet.

Nearby diagnostics

Other rules in this family

All 37 rules