库中对外暴露类是否需同时实现IDisposable与IAsyncDisposable?
Great question—this is such a common and tricky pain point when working with async resources in .NET. Let’s break this down:
First, your core concern is spot-on
Implementing IDisposable alongside IAsyncDisposable and having Dispose() block on DisposeAsync().GetAwaiter().GetResult() is extremely risky. In contexts with a synchronization context (like UI threads in WPF/WinForms, or older ASP.NET environments), this is a classic recipe for deadlocks. Even outside those contexts, you’re tying up a thread waiting on an async operation, which wastes resources and can lead to thread pool exhaustion under load. I’ve definitely run into this exact issue before:
- In a WPF app, a team member wrote a class with this pattern, and using it in a
usingblock on the UI thread caused the app to freeze solid—turns out the async disposal logic needed to marshal back to the UI context, which was blocked waiting for the operation to finish. - In an ASP.NET Web Forms project, repeated synchronous calls to this kind of
Dispose()led to thread pool starvation during peak traffic, since multiple worker threads were stuck waiting on async I/O.
Your try-finally approach: good intentions, but not ideal
Forcing users to write explicit try-finally blocks to call DisposeAsync() manually does prevent accidental blocking—but it’s clunky and error-prone. For one, your example code has a subtle bug: if new MyClass() throws an exception, c will be uninitialized, and the finally block will throw a NullReferenceException. You’d need to initialize c to null first and add a null check, which adds even more boilerplate.
Worse, users will inevitably forget to write the finally block, or mess up the null checking, leading to resource leaks. It’s not a sustainable pattern for a library—you want to make the correct usage as easy as possible, not as hard as possible.
A better alternative: lean into await using
Starting with .NET Core 3.0 and .NET 5+, the language supports the await using statement specifically for types that implement IAsyncDisposable. If you only implement IAsyncDisposable (and omit IDisposable), users will get a compiler error if they try to use a regular using block—this forces them to use the correct async syntax:
// This works perfectly, no blocking await using var c = new MyClass(); // Use c here
The compiler handles the async cleanup automatically, just like regular using does for synchronous disposal. This is far more ergonomic than manual try-finally, and eliminates the risk of accidental blocking entirely.
When might you still need IDisposable?
If your library needs to support older .NET versions that don’t have await using (like .NET Framework < 4.8), you might have to implement IDisposable—but proceed with caution. Some options here:
- Document the risk heavily: Clearly state in your XML docs that the synchronous
Dispose()is not recommended for contexts with a synchronization context, and preferDisposeAsync()when possible. - Throw a helpful exception: In
Dispose(), check if a synchronization context exists (viaSynchronizationContext.Current) and throw anInvalidOperationExceptiontelling users to useawait usinginstead of blocking. This prevents accidental deadlocks instead of letting them happen silently. - Use
ConfigureAwait(false): If yourDisposeAsync()logic doesn’t need to return to the original context, you can modify the synchronousDispose()to useDisposeAsync().ConfigureAwait(false).GetAwaiter().GetResult()—this reduces (but doesn’t eliminate) deadlock risk.
Final takeaway
Your instinct to avoid synchronous blocking is correct. Instead of forcing manual try-finally, the best approach for modern .NET is to only implement IAsyncDisposable and let the compiler guide users to await using. This keeps your library safe, ergonomic, and aligned with .NET’s async best practices.
内容的提问来源于stack exchange,提问作者Greg

