If you check an official C# language versioning page you might think that there is a very strong relationship between the target framework and the C# language version.

And indeed, if you won’t specify the C# language version implicitly in the project file the version would be picked based on the target framework: C# 12 for .net8, C#11 for .net7, and C# 7.3 for Full Framework:

CSharp_Lang_Versions

And even though the mapping just specifies the defaults, some people believe that the mapping is fixed and, for instance, if you got stuck with Full Framework, you also got stuck with C# 7.3. But this is not the case.

The actual relationship between the C# language version and the target framework is more delicate.

There are 3 ways how the feature might relate to the target framework.

  • Just works. Some features like enhanced pattern matching, readonly struct members, enhanced usings and static lambdas, just work out of the box. Just set the right langVersion and new feature works regardless of the target framework.
  • Requires a special type definition. Other features, such as new interpolated strings, non-nullable types, ranges and many other language features requires special types to be discoverable by the compiler. These special types are added to .net core release that corresponds to particular C# version, but you can add them manually to your compilation (see examples below) to get a feature working.
  • Runtime specific. And only a small fraction of all the new language features do require the runtime support. Features like Default Interface Implementations, Inline Arrays or ref fields won’t compile if the target framework doesn’t support it and if you’ll try, you’ll get an error: Error CS9064 : Target runtime doesn't support ref fields.

The first and the last cases are quite obvious, but the second one requires a bit of extra information. The C# compiler requires the special types to be available during compilation of the project if the feature is used, and it doesn’t care where the type definition is coming from: it can come from the target framework, from a nuget package, or be part of the project itself.

Here is an example of using init-only setters (available since C# 9) in a project targeting netstandard 2.0:

// Project targets netstandard2.0 or net472
public record MyRecord
{
    // System.Runtime.CompilerServices.IsExternalInit class is required.
    public int X { get; init; }
}

namespace System.Runtime.CompilerServices
{
    internal class IsExternalInit { }
}

But if you’ll try to use some other features, like required members, the amount of manual work would be way higher:

public record class MyRecord
{
    // System.Runtime.CompilerServices.IsExternalInit class is required.
    public int X { get; init; }
    // System.Runtime.CompilerServices.RequiredMemberAttribute,
    // CompilerFeatureRequiredAttribute and
    // System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute are required
    public required int Y { get; set; }    
}

namespace System.Runtime.CompilerServices
{
    internal class IsExternalInit { }
    internal class RequiredMemberAttribute : System.Attribute { }
    internal sealed class CompilerFeatureRequiredAttribute(string featureName) : System.Attribute
    {
        public string FeatureName { get; set; } = featureName;
    }
}

namespace System.Diagnostics.CodeAnalysis
{
    internal class SetsRequiredMembersAttribute : System.Attribute { }
}

Adding all the attributes manually to every project is very tedious, so you can rely on some MSBuild magic to add a set of known files based on the target framework. Or you could just use something like PolySharp that uses source generation to add all the required types regardless of the target framework.

C# 12 Features

Language Feature Requirements
ref-readonly parameters No extra requirements(*)
Collection expressions No extra requirements
Interceptors InterceptsLocationAttribute (**)
Inline Arrays Runtime support is required: .net8+
nameof accessing instance members No extra requirements
Using aliases for any types No extra requirements
Primary Constructors No extra requirements
Lambda optional parameters No extra requirements
Experimental Attribute ExperimentalAttribute (***)

(*) ref-readonly parameters is an interesting feature. On one hand, it doesn’t require any extra types to be declared manually, but it does rely on an extra type - System.Runtime.CompilerServices.RequiresLocationAttribute. But if the compilation is missing this type, the compiler would generate it for you!

() The full type name is System.Runtime.CompilerServices.InterceptsLocationAttribute (*) The full type name is System.Diagnostics.CodeAnalysis.ExperimentalAttribute

C# 11 Features

Language Feature Requirements
   

Here is a table with all the features from C# 8 to 12 with its category and the requirements, like, a special type or a runtime that started supporting it. Here is a table that shows all the features for C# 8 to 12 and

Let’s say you’re using the latest compiler (Feb 2023, the compiler suppoerts C# 12):

This lead to a ton of confusion, since people might think that, for instance, if you target .NET Framework you have to stay with version 7.3. But you have to remember, that this