Featured image of post How to use C# 11 features in .NET 6 or older versions (even .NET Framework 2.0)

How to use C# 11 features in .NET 6 or older versions (even .NET Framework 2.0)

C# 11 on AWS Lambda and Dataverse plugins

Follow me

Introduction Link to this section

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? Link to this section

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? Link to this section

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 Link to this section

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 Link to this section

Here are some examples of the most useful features of latest versions of C# (not only C# 11).

Top-level statements Link to this section

No need to static void Main:

1
2
3
4
5
using System;

Console.WriteLine("Hello World");

Console.ReadKey();

Nullable reference types Link to this section

This is a working example of nullable reference types and top-level statements in .NET Framework 2.0:

1
2
3
4
5
6
7
8
9
#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 Link to this section

Pattern matching also works in .NET Framework 2.0:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 Link to this section

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).

Missing type errors for records, init and required properties

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 Link to this section

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 Link to this section

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]

Example from PolySharp docs

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 from package.config to Package Reference worked for me.

On Visual Studio, click with the right mouse button in References and select Migrate package.config to Package Reference, confirm the changes and we are done: Migrate package.config to Package Reference

Required keyword, init-only properties and records Link to this section

Here is an example of a .NET Framework 4.7.2 application using the the required keyword, init-only properties and records:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#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:

RequiredMemberAttribute code generated by PolySharp

IsExternalInit code generated by PolySharp

⚠️ Record types require .NET Framework 4 or superior runtimes.

Source code of the examples Link to this section

https://github.com/dgenezini/CSharpNewestFeatures

💬 Like or have something to add? Leave a comment below.
Ko-fi
GitHub Sponsor
Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy