Enabling and using C# 9 features on older and “unsupported” runtimes
By: Sergio Pedri
Tags: Microsoft, Csharp, Dotnet, Dotnet Core, Programming

Level 2: what we can enable support for

While we still have some features left in C# 8 and 9 that can work on our older targets, these remaining ones will need some extra care in order to make the Roslyn compiler happy. Specifically, these are features that are backwards compatible, but do need some specific types or attributes to be present in the project. The good news is that if we are missing libraries we can just reference them from NuGet, and if we miss types we can literally just copy them from the CoreCLR source on GitHub and paste them into our project! The C# compiler does not actually need those types to be built-in — as long as it can find them in the right place, that will be enough for it to successfully build our code. So, let’s go through all the remaining features one by one!

C# 9 records and init-only properties

The new record types introduced in C# 9 can greatly reduce the amount of boilerplate code that is required to implement simple models, but they are one of the features that needs some help to work properly on older targets. Specifically, the C# compiler will need the special IsExternalInit type to be present in our project. To enable support for this, we can simply copy the following code anywhere in your our and then try building again!

C# 9 [SkipLocalsInit] attribute

This new attribute is useful in performance critical scenarios, and instructs the compiler not to emit the .locals init flag for variables in a method. This can avoid unnecessary zero-initializations of variables and improve performance. The attribute is not available on older runtimes, but as with records, we simply need to copy it into our project and then use it normally. Roslyn will recognize it and use it just fine, since it just checks the type name and namespace, and not the assembly the type actually belongs to. Here it is:

C# 8 asynchronous streams and disposables

This feature relies on the IAsyncEnumerable<T> and IAsyncDisposable types, and allows us to write asynchronous enumerators, to use the new await foreach construct and to implement asynchronous dispose methods to enable the new await using syntax. All we need to do in this case is to add a reference to the Microsoft.Bcl.AsyncInterfaces NuGet package, which includes all these necessary types. Once we have a reference to that package we will be able to use all of those new APIs with no problems at all:

Flow analysis attributes

You might have noticed that, especially with respect to nullable reference types, not all the necessary attributes to decorate fields, properties, methods and other members are available, especially when working on .NET Standard 2.0 and below. These are attributes like [MaybeNullWhen],[NotNullWhen] and others detailed in the docs that enable better static analysis, as they can give the compiler more information about how our code is interacting with values. Thankfully, we can just backport all of these from the CoreCLR source and then use them just like we would have done if they had already been available in the framework we are working with. Here is a gist with all the currently available attributes ready to use — we can just paste this file into our project and start using them wherever we need: