If you haven't heard, C# 8.0 is not supported on anything below .NET Core 3. This is in part due to the fact that some of the features cannot run on runtimes below .NET Core 3, and rather than complicate things, C# 8.0 is considered an all-or-nothing choice.

This presents a bit of a problem, because you might be tempted to use some the compelling new features in a lower target, particularly if you are multi-targeting. For example, a lot of people are going to want to additionally target netstandard2.0 for a broader reach, probably at least up until November 2020 when .NET 5 is due.

Here is a slide a from by Mads Torgersen at .NET Conf, saying that for library authors you want to consider targeting both netcoreapp3.0 and netstandard2.0, and use C# 8.0 to get "nullified" during this "Nullable adoption phase". You can catch the full video here.

Slide from What's new in C# 8 with Mads Torgersen at .NET Conf

So the good news is that you can enable C# 8.0 on other targets, however it is "unsupported" i.e. this is probably not a good idea, but if you don't mind dealing with some hassle, let's do this! 🤠

Types of Language Features

The new language features can roughly be viewed as being in one of 3 categories:

  1. Syntax Only - Language features that are purely syntax will "Just Work™".
  2. Requires Types - Some language features rely on new types that are all present in .NET Core 3; if we are going to use these features we need to satisfy the compiler and runtime. Sometimes the shape of a type is enough to light up a feature, sometimes it needs to be a particular name, namespace and implementation.
  3. Requires Runtime Support - If the feature needs to utilise a new runtime feature that ain't there, it just ain't gonna work!

C# 8.0 Feature Compatibility

Feature Support Category
Static local functions 1
Using declarations 1
Null-coalescing assignment 1
Readonly members 1
Disposable ref structs 1
Positional patterns 1
Tuple patterns 1
Switch expressions 1
Nullable reference types 1 (and a little bit of 2)
Asynchronous streams 2
Indices and ranges 2
Default interface members 3 (Will not work 💥)

So you can see, categorising the new features, there's a lot we can use "for free" whilst in this unsupported mode. In particular, two features I'd encourage anyone to adopt are switch expressions and using declarations. Here's a good example:

switch expressions and using declarations 👌

Feature Break Down

Default Interface Members

This feature requires a runtime that supports it, so if you are targeting a runtime that doesn't have that support, it's never going to work. The compiler helps us out here by giving us a clear error message if we try:

error CS8701: Target runtime doesn't support default interface implementation.

Async Streams

Async streams is the ability to use IAsyncEnumerable at the language level. One way of doing this is with await foreach, the other is with async iterator methods that let you yield await.

To use this feature in netstandard2.0, you can pull in a reference to this package:

Microsoft.Bcl.AsyncInterfaces 1.0.0
Provides the IAsyncEnumerable<T> and IAsyncDisposable interfaces and helper types for .NET Standard 2.0. This package is not required starting with .NET Standard 2.1 and .NET Core 3.0. Commonly Used Types: System.IAsyncDisposable System.Collections.Generic.IAsyncEnumerable System.Collections.Generi…

This package defines the IAsyncEnumerable interface for platforms that don't officially support C# 8.0. This is a really big deal, because it means you can start exposing this type today in your library APIs and know that people can reasonably consume it from earlier Target Framework Monikers (TFMs).

Indices and Ranges

Indices and Ranges is a lovely little feature, for example it lets us trivially slice things like this:

var chopFirstAndLast = "~abc~"[1,^1]; // = "abc"

On the surface, it may appear to be syntax only, but this compiles down to use two types, Index and Range. The compiler looks for these by fully qualified name, so all we need to do is create those types in our project and it will work.

While this does work, potentially there is a problem if we expose one of these types in our public API. Consuming libraries will see Index or Range in the System namespace, but that would clash with their own implementations, or the implementation from supported TFMs.

To solve this, we could put these types in a NuGet package and type forward to the real implementations on the supported runtimes. I have done this here:

Contrib.Bcl.Ranges. Contribute to slang25/csharp-ranges-compat development by creating an account on GitHub.

However I have not distributed it yet, as for it to be generally useful for the community, it must be the canonical package used amongst consumers that want to interoperate with it, and I want to think about having maintained in a more central place.

This library also contains extension methods to types like Array and String to match those in the supported TFMs, as that is where the syntax is most useful.

Nullable Reference Types

This is the big one! If you are still reading this, odds are you are looking because you want to know about Nullable Reference Types.

The way this feature works is by the compiler emitting attributes on methods, parameters, fields etc... indicating their nullability. These attributes (Nullable and NullableContext) are actually emitted by the compiler as internal types and are placed in your assembly. So this "Just works™"!

However, there is another useful subset of this feature that you might want to use, and that is the attributes that you would add to indicate the relationship of nullability between method parameters and return values. An often used example for this is the familiar string.IsNullOrEmpty, here an attribute is used to say that when this method returns true the parameter cannot have been null. This allows for more accurate flow analysis and makes this language feature more usable. You can find find these under System.Diagnostics.CodeAnalysis, and only exist for supported TFMs.

You can however reference this NuGet package by Manuel Römer:

A source code only package which allows you to use .NET Core 3.0's new nullable attributes in older target frameworks like .NET Standard 2.0 or the "old" .NET Framework. - manuelroeme...

This package is a source package, so it will include these nullable attributes as source at build time. The nice thing about this is that you can reference the package in your csproj with PrivateAssets="all", which will mean that this doesn't become a dependency for anyone depending on your code.

To be clear, you don't need this package, but it will come in handy from time to time, and with this package you do not need to compromise while targeting netstandard2.0 for example.

An alternative to this package is ReferenceAssemblyAnnotator, this also provides the attributes and additionally weaves in nullability into .NET Framework and .NET Standard reference assemblies.

For a deeper dive into Nullable Reference Types, you should check out Cezary Piątek's article here.

If you take away one thing from this post, please let it be this:

While Nullable Reference Types as a feature is "not supported" in lower TFMs such as netstandard2.0, it is encouraged to use this feature, and you can do it with little hassle.


Putting some of these thing together, I managed to get some C# 8.0 features running under mono targeting net461 on my mac, you can see that here:

Contribute to slang25/csharp8_net4x_playground development by creating an account on GitHub.

If there is anything I've missed, please let me know in the comments.