The Tale of the Async Interfaces
# Dec 15, 2020 by StevenWith the release of Simple Injector v5, I made an error of judgement. To support asynchronous disposal of the
Container
andScope
objects I added a dependency from the core library to the Microsoft.Bcl.AsyncInterfaces NuGet package. Unfortunately, this proved to be a very painful mistake. In this blog post I’ll explain why I choose to take this dependency, why this was a mistake, and how this finally got fixed in v5.2.
Double Trouble
With the introduction of .NET Core 3, Microsoft added a new IAsyncDisposable
interface as asynchronous counterpart of IDisposable
. In order enable asynchronous disposal in older framework versions (i.e. .NET Core 2.0 and .NET 4.6.1), Microsoft published the Microsoft.Bcl.AsyncInterfaces NuGet package. Where the package’s net461
and netstandard2.0
targets contain assemblies that specify—among other things—the IAsyncDisposable
interface, the netstandard2.1
target assembly uses type forwarding to reference the framework’s version.
The publication of the AsyncInterfaces NuGet packages allowed Simple Injector v5.0 to start using the IAsyncDisposable
interface. This allowed Simple Injector to asynchronously dispose of classes implementing IAsyncDisposable
. It also allowed users to dispose of both Scope
and Container
in an asynchronous fashion.
Historically, the Simple Injector core library hasn’t taken a dependency on an external NuGet package. And for good reason. Because external dependencies can cause conflicts, such as those pesky binding-redirect issues that we’re all too familiar with. This is an unfortunate restriction and there have been many times I wished I could just pull in some external package to simplify my work. The very useful System.Collections.Immutable package is one such example.
Unfortunately, there is no easy way of allowing Simple Injector to asynchronously dispose of registered types without taking a dependency on AsyncInterfaces. This left me with two options:
- Add this dependency to the core and implement asynchronous disposal directly in the core library
- Create a new NuGet package (e.g.,
SimpleInjector.Async
) that adds extensions that allow asynchronous disposal ofScope
andContainer
.
I decided to go with the first option as the second would result in a less discoverable solution. The former option allows implementing IAsyncDisposable
directly on Container
and Scope
; the latter would likely rely on extension methods, to allow something similar to the following: await scope.AsAsyncDisposable().DisposeAsync();
.
This new feature was implemented and introduced in Simple Injector v5. This is when the trouble started.
Diamonds are forever
Within a few days of the release of v5, developers began reporting binding-redirect issues. This was caused by the multitude of versions that exist for the AsyncInterfaces package and its dependencies. Developers were using other libraries and framework parts that took a dependency on AsyncInterfaces, or its dependencies System.Threading.Tasks.Extensions and System.Runtime.CompilerServices.Unsafe. When one of the used application dependencies references a different version of such package (and in particular when the package’s contained assembly has a different assembly version) binding-redirect issues can appear.
In an ideal world, the NuGet package manager automatically solves binding-redirect issues for us by adding the required plumbing in the application’s configuration file. But for some reason, the package manager fails to do this. Instead, we have to manually add binding redirects to our configuration files. And this seems especially confusing with regards to AsyncInterfaces and its sub dependencies, as their assembly versions do not match the NuGet package version. The dependency chain of AsyncInterfaces seems to be in the list of common troublemakers.
With the introduction of .NET Core, there’s the idea that binding-redirect issues are thing of the past. A more-recent bug report, however, demonstrated that this isn’t always the case, demonstrating that these issues won’t easily go away if we wait long enough.
The pain experienced with these dependencies can be solved by setting the correct binding redirects in the application’s configuration file. To help Simple Injector users, I started posting binding direct examples that developers could copy-paste to fix their problem. But even using these examples, developers struggled, and I recently got stuck with this on an application I was developing. Whatever binding redirects I tried, after analyzing the assembly versions of the used NuGet packages, the application would crash with a dreaded “Could not load file or assembly X or one of its dependencies” exception. This was the moment that I started to realize the gravity of this Diamond Dependency dilemma that AsyncInterfaces and his little helpers caused. Action was required. This instigator had to go.
Diamond Dependency
My application took a dependency on both Simple Injector and System.Collections.Immutable. Those two libraries, however, both depended (indirectly) on the previously mentioned CompilerServices.Unsafe. Simple Injector did so via AsyncInterfaces and Tasks.Extensions, while Immutable depended on CompilerServices.Unsafe via System.Memory. This is an example of a Diamond Dependency.
A Diamond Dependency is a dependency chain of at least four libraries where library A (e.g. my application) depends on libraries B (Simple Injector) and C (Immutable). B and C than depend on the final library D (Unsafe). A problem emerges when those middle libraries require different versions of this final library D. This is called the Diamond Dependency Conflict.
If it quacks like a duck
But removing the AsyncInterfaces dependency was easier said than done. Removing the asynchronous disposal feature was not an option; developers already depend on that feature, often—but not always —implicitly by using one of the ASP.NET Core integration packages. And we can certainly expect asynchronous disposal to become more common soon, long before the Diamond Dependency problem will disappear (i.e. before Simple Injector can drop support for .NET 4 and .NET Core 2).
Over the last 6 months I have conducted two experiments that tried duck typing to allow removing the AsyncInterfaces dependency from the core library. With duck typing, instead of depending on interfaces, you take a more dynamic approach where you accept any type that conforms to a certain signature. The C# compiler takes this approach in many places, for instance with the foreach
, using
, and async using
keywords. Both trials were discontinued because of the complexity they would introduce into the library, especially when taking performance into consideration. But after I recently experienced the seriousness of the situation myself, I knew removing the party crasher was the only viable solution to this problem. And so, I had to release the Quacken!
After multiple days of trying, failing, testing, improving, asking, things started to quack like duck. The meat and potatoes of the implementation is inside the Scope
class (which now is littered with compiler directives). Scope
now does the following:
- It tries to figure out if a tracked instance implements an interface named “System.IAsyncDisposable”. Whether that interface is The Real Thing ™ or just some self-defined surrogate is irrelevant. If the instance implements that interface, it is stored for disposal, as would happen for ‘normal’
IDisposable
instances. - During asynchronous disposal of the
Scope
, theScope
will invoke the instance’sDisposeAsync
method. The returned object (typicallyTask
orValueTask
) will be awaited.
Of course, performance must be taken into consideration, considering that Reflection calls are slow. Such a performance penalty would perhaps be acceptable during disposing of the Container
, but certainly not when disposing of a Scope
, as an application might create and dispose of thousands of Scope
instances per second. And so Scope
implements the following caching:
- Whether a checked type implements
IAsyncDisposable
or not. This way only one call toType.GetInterfaces().Where(i => i.FullName == "System.IAsyncDisposable")
is required. - When the
IAsyncInterface
is detected for the first time, it will be stored internally. This allows any subsequent checks to callasyncDisposableType.IsAssignableFrom(type)
instead of the slowerGetInterfaces().Where(...)
again. - The call to
IAsyncDisposable.DisposeAsync().AsTask()
is compiled using expression trees, just as the rest of Simple Injector does for object composition under the covers. This makes callingDisposeAsync
(almost) as fast as a native interface call. The .NET Standard 2.1 version, btw, completely skips all this duck typing nonsense and just natively callsIAsyncDisposable
because, as I mentioned previously, with .NET Core 3 that interface is recognized natively.
The greatest disadvantage of this approach, from a user’s perspective, is that I had to remove the DisposeAsync
methods from Container
and Scope
in the pre-.NET Standard 2.1 builds. Because not only did the removal of AsyncInterfaces mean no reference to IAsyncDisposable
, it also removed the reference to ValueTask
, which the IAsyncDisposable.DisposeAsync
method returns. For a while I played with the idea of the other builds to have an DisposeAsync
method that would simply return Task
. This would allow developers to use C#’s async using
syntax on Container
and Scope
. But I quickly realized that the different signature of the DisposeAsync
method (the return type is part of the signature) would cause MissingMethodExceptions
.
To prevent incompatible signatures, while still allowing both the Container
and Scope
to be disposed of asynchronously, methods with completely different names needed to be added. This is why you’ll find DisposeContainerAsync()
and DisposeScopeAsync()
methods on Container
and Scope
respectively. I’m the first to agree that this is bats-ugly, but it’s the best I could come up with.
On the flip side, however, because of the use of duck typing, Simple Injector can now support asynchronous disposal on all of its builds. Where previously asynchronous disposal was only supported on the net461
and netstandard2.0
builds of Simple Injector, with the introduction of Simple Injector v5.2, asynchronous disposal is supported on net45
and netstandard1.0
as well. Although its not possible to reference AsyncInterfaces’ IAsyncDisposable
in your application, you can simply define System.IAsyncDisposable
somewhere in your application, and it just works. Here’s an example:
namespace System
{
public interface IAsyncDisposable
{
Task DisposeAsync();
}
}
Even though the official IAsyncDisposable
interface exposes ValueTask
rather than Task
, Simple Injector accepts this alternative definition anyway. As long as the interface is called “System.IAsyncDisposable” and there’s a method named “DisposeAsync” which either returns Task
or ValueTask
, everything will just run smoothly. This allows you to start using asynchronous disposal until you can migrate to your code base to .NET Core 3 or .NET 5.
All the sweat and tears I poured over my keyboard in the past weeks to get this fixed are now dried up and materialized in the Simple Injector code v5.2 code base. This will certainly not fix all your binding-redirect issues but will at least ensure that Simple Injector is not amplifying the problem any longer.
Happy injecting.