One of the awesome features introduced in .NET Core 3.0 and C# 8.0 are async streams. The feature consists of two parts - async disposable, for async clean up, as well as async enumerable, for async iteration.
Normally, the C# language features are backwards compatible and can be used regardless of the runtime framework being targeted. In this particular case, however, the newly introduced types that are needed for async streams feature to work, such as for example IAsyncDisposable or IAsyncEnumerator
Let’s have a look at how you can still benefit from async disposable and async enumerable on older frameworks.
Async disposable in .NET Core 3.0 π
Let’s start by focusing on async disposable as it’s simpler and easier to discuss. Below you can find a very simple example of using async disposable in C# 8.0.
class Program
{
static async Task Main(string[] args)
{
await using (var disposableObject = new Foo())
{
Console.WriteLine("Hello World!");
}
Console.WriteLine("Done!");
}
}
class Foo : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
Console.WriteLine("Delaying!");
await Task.Delay(1000);
Console.WriteLine("Disposed!");
}
}
When this code is executed, the following gets printed out:
Hello World!
Delaying!
Disposed!
Done!
There is of course a visible delay between Delaying! and Disposed! since we put in a
Task.Delay there to showcase the async aspect of the dispose. The value proposition is the ability to do async clean up work on our resources (instead of having to block in traditional IDisposable), as well as the nice terse syntax of C# 8.0 - await using β¦.
In order for this code to compile, we need to configure our project to use netcoreapp3.0 target framework.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
</Project>
The reason for this is that IAsyncDisposable, as mentioned, is only available in .NET Core 3.0 and .NET Standard 2.1.
Async disposable in .NET Core 2.1 π
Now, if you want to make async disposable work in .NET Core 2.1, you cannot just target netcoreapp2.1 straight up.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
</Project>
If you try that, using the same code as we have above, you’d get the following compiler error upon compilation:
error CS0246: The type or namespace name 'IAsyncDisposable' could not be found (are you missing a using directive or an assembly reference?)
The same applies if we tried to use netstandard2.0 or net472 or any other framework not compatible with .NET Standard 2.1. That is pretty clear - since we already mentioned that this type did not exist in the BCL before .NET Core 3.0 shipped. However, what is not commonly known is that there is no actual functionality in the runtime to make the async disposable work. Instead, the type is needed as the so-called “well-known” type for the compiler. And to satisfy the compiler, the actual assembly location of the type doesn’t matter - as long as the type structure, name and namespace are correct. This “duck typing” or “polyfilling” is a bit counter-intuitive in the C# world, but it really works in this case.
So we can just add the following interface to our program:
namespace System
{
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
}
And simply run the program again. The well-known type requirement for the compiler will be satisfied, and the program will compile and work fine. So our code now looks like:
class Program
{
static async Task Main(string[] args)
{
await using (var disposableObject = new Foo())
{
Console.WriteLine("Hello World!");
}
Console.WriteLine("Done!");
}
}
class Foo : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
Console.WriteLine("Delaying!");
await Task.Delay(1000);
Console.WriteLine("Disposed!");
}
}
namespace System
{
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
}
Do note that the interface requires ValueTask which is only available in .NET Core 2.0+, so this approach wouldn’t work elsewhere (i.e. in .NET Standard 2.0 / .NET 4.6.1). However for that there is an even better solution.
Async disposable and enumerable everywhere π
While there is something thrilling about being able to duck type an interface into your program and make a C# 8.0 language feature light up, you don’t actually need to do that. Turns out, the folks from the dotnet team (CoreFX team) have actually published a “compatibility bridge” package, called Microsoft.Bcl.AsyncInterfaces which enables both async disposable as well as async enumerable on the runtimes older than .NET Core 3.0.
It is enough to just reference the nuget package, and it brings in the well-known types needed by the compiler. For example, we could successfully build our program for desktop .NET 4.6.1 by merely changing our csproj file accordingly:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="1.0.0" />
</ItemGroup>
</Project>
To circle back on our “manual” duck typing that we used before to enable async disposable on .NET Core 2.1 - async enumerable could also be enabled the same way there, by simply adding a few necessary types, but there is quite a few of them, so it’s generally more convenient to just reference the above nuget package.
By the way, the package is no longer produced by the CoreFX team and has been removed from the source of CoreFX - it is however still listed and available on nuget.
The approach of “polyfilling” certain missing well-known types for the compiler is not entirely new too, there were features that shipped like that before - for example ValueTuple. Another cool example is the community built, string interpolation bridge, which allowed C# 6 string interpolation to be used pre-.NET 4.6, by providing the missing FormattableString and FormattableStringFactory types.
Hopefully this post will help you get up and running with async enumerable and async disposable on older frameworks too!