How to use C# 11 features in .NET 6 or older versions (even .NET Framework 2.0)
Tags: dotnet, csharp, webdev, programming
Published: 2023-02-06 23:16:49

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

```xml
<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`:

```csharp
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:

```csharp
#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](https://blog.genezini.com/p/compile-time-null-safety-how-to-avoid-nullreferenceexception-in-c/).

> ℹ️ **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](https://github.com/tunnelvisionlabs/ReferenceAssemblyAnnotator).

### Pattern Matching

Pattern matching also works in .NET Framework 2.0:

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

![Missing type errors for records, init and required properties](https://blog.genezini.com/p/how-to-use-c-sharp-11-features-in-dotnet-6-or-older-versions/images/missing-types.png)

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]

![Example from PolySharp docs](https://blog.genezini.com/p/how-to-use-c-sharp-11-features-in-dotnet-6-or-older-versions/images/polysharp-examples.png)

To install it, just add its NuGet package:

```bash
Install-Package PolySharp
```

> ⚠️ Because PolySharp uses source generators, it doesn't work with the `package.config` file as stated in [this issue](https://github.com/Sergio0694/PolySharp/issues/39). 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](https://blog.genezini.com/p/how-to-use-c-sharp-11-features-in-dotnet-6-or-older-versions/images/migrate-packages-config.png)

### 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:

```csharp
#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](https://blog.genezini.com/p/how-to-use-c-sharp-11-features-in-dotnet-6-or-older-versions/images/requiredmemberattribute-generated.png)

![IsExternalInit code generated by PolySharp](https://blog.genezini.com/p/how-to-use-c-sharp-11-features-in-dotnet-6-or-older-versions/images/isexternalinit-generated.png)

> ⚠️ 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](https://blog.genezini.com/) to see.

## Follow me

- [On Feedly](https://feedly.com/i/subscription/feed/https://blog.genezini.com/index.xml)
- [RSS](https://blog.genezini.com/index.xml)
- [Linkedin](https://www.linkedin.com/in/danielgenezini/)
- [Twitter](https://twitter.com/GeneziniDaniel)

## References and Links

- [PolySharp GitHub Page](https://github.com/Sergio0694/PolySharp)
- [Sergio Pedri post about PolySharp](https://sergiopedri.medium.com/enabling-and-using-c-9-features-on-older-and-unsupported-runtimes-ce384d8debb)