At each release, C# adds features that help us make our codes cleaner, more readable and more maintainable. The problem is that, because some features are dependent of runtime implementations, C# versions are generally tied to .NET runtime versions. For example, C# 11 is enabled only in .NET 7 and above.
In this post, I’ll show how to use C# 11 in older runtime version (even .NET Framework 2.0).
Why not upgrade the .NET version?
Upgrading to the newest .NET version is the best option. Not only we benefit from new C# features, but also from performance and security improvements.
But there are some scenarios where upgrading is not an option because of compatibility or because the cost of upgrading would be too high.
Some examples are:
- AWS Lambda Functions running on .NET 6 (AWS Lambda doesn’t support .NET 7 at the time of this post);
- Plugins or extensions for proprietary software, such as Dynamics/Dataverse plugins that are not compatible with .NET Core
- Legacy systems with a large codebase that still receive frequent updates.
What C# 11 features can be used?
C# features are divided in features that require runtime support and features that are just syntactic sugar.
Features that require runtime support cannot be used in older .NET versions, but most of the features that are syntactic sugar are compiled to IL (.NET Intermediate Language) and interpreted by older .NET versions at runtime (Even .NET Framework 2.0), depending only on an updated version of Roslyn (the .NET compiler) to work.
How to use C# 11 features in .NET 6 and previous versions
Some features will work just by having the .NET 7 SDK installed and adding (or updating) the
LangVersion tag to
11 in the
Here are some examples of the most useful features of latest versions of C# (not only C# 11).
No need to
static void Main:
Nullable reference types
This is a working example of nullable reference types and top-level statements in .NET Framework 2.0:
💡 We can even treat warnings as errors, as I explained in this post.
ℹ️ If the project doesn’t use the new csproj format, the
<Nullable>enable</Nullable>won’t be interpreted and the use of the
#nullable enabledirective at the start of each file is required.
⚠️ One caveat of using nullable types in older .NET versions is that framework functions won’t inform us if they return nullable reference types because these changes were implemented only in the newer .NET versions.
EDIT: A reader reached me about this cool package that partially solves this problem by injecting nullable reference type annotations in CLR’s methods of some assemblies (check the docs for more details): ReferenceAssemblyAnnotator.
Pattern matching also works in .NET Framework 2.0:
⚠️ List pattern matching won’t work just by changing the
LangVersiontag. It needs specific types that I’ll explain in the next section.
Features that need specific types
Even for features that are syntactic sugar, some depend on types and attributes implemented in the newer CLRs (for instance, the list pattern matching and the required keyword).
If we copy those types from the CLR source code or reference them from NuGet packages, the compilation will succeed and the features will be available.
But there is a better alternative…
PolySharp is a NuGet package created by Sergio Pedri, Software Engineer at Microsoft, that generates polyfills for those types at compile time, only for the features being used in the code and that are not present in the targeted runtime.
Features enabled by PolySharp
This is a shortened list of some C# features enabled by PolySharp:
- Nullability attributes
- Index and Range
- List pattern matching
- Required members
- Init-only properties
To install it, just add its NuGet package:
⚠️ Because PolySharp uses source generators, it doesn’t work with the
package.configfile as stated in this issue. The issue says we need to use the SDK style
.csproj, but just changing from
Package Referenceworked for me.
On Visual Studio, click with the right mouse button in
Migrate package.config to Package Reference, confirm the changes and we are done:
Required keyword, init-only properties and records
Here is an example of a .NET Framework 4.7.2 application using the the required keyword, init-only properties and records:
If we inspect the
IsExternalInit, we can see they were generated by PolySharp:
⚠️ Record types require .NET Framework 4 or superior runtimes.
Source code of the examples