Introduction
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 csproj
file.
<LangVersion>11</LangVersion>
Examples
Here are some examples of the most useful features of latest versions of C# (not only C# 11).
Top-level statements
No need to static void Main
:
using System;
Console.WriteLine("Hello World");
Console.ReadKey();
Nullable reference types
This is a working example of nullable reference types and top-level statements in .NET Framework 2.0:
#nullable enable
using System;
string? nullHere = null;
Console.WriteLine($"Length: {nullHere?.Length}");
Console.ReadKey();
💡 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 enable
directive 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
Pattern matching also works in .NET Framework 2.0:
using System;
Console.WriteLine($"Enter the water temperature in Fahrenheit:");
var isNumber = int.TryParse(Console.ReadLine(), out var number);
string GetWaterState(int tempInFahrenheit) =>
tempInFahrenheit switch
{
(> 32) and (< 212) => "liquid",
< 32 => "solid",
> 212 => "gas",
32 => "solid/liquid transition",
212 => "liquid / gas transition"
};
if (isNumber)
{
var waterState = GetWaterState(number);
Console.WriteLine(waterState);
}
else
{
Console.WriteLine("Invalid number");
}
Console.ReadKey();
⚠️ List pattern matching won't work just by changing the
LangVersion
tag. 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...
Enter PolySharp
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
- [CallerArgumentExpression]
- [StringSyntax]
To install it, just add its NuGet package:
Install-Package PolySharp
⚠️ Because PolySharp uses source generators, it doesn't work with the
package.config
file as stated in this issue. The issue says we need to use the SDK style.csproj
, but just changing frompackage.config
toPackage Reference
worked for me.On Visual Studio, click with the right mouse button in
References
and selectMigrate 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:
#nullable enable
using System;
var person = new Person() { FirstName= "Sherlock", LastName = "Holmes" };
var address = new Address("Baker Street 221b", "London");
Console.WriteLine($"Person: {person.FirstName} {person.LastName}");
Console.WriteLine($"Address: {address}");
Console.ReadKey();
record Address (string StreetName, string City);
class Person
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
}
If we inspect the RequiredMemberAttribute
and IsExternalInit
, we can see they were generated by PolySharp:
⚠️ Record types require .NET Framework 4 or superior runtimes.
Source code of the examples
https://github.com/dgenezini/CSharpNewestFeatures
Liked this post?
I post extra content in my personal blog. Click here to see.