If you check the 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. Set the right langVersion in a project file and a 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 some others require 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 the features 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 for the feature to be usable, and it doesn’t care where the type definition is coming from: 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, you would have to add quite a bit of extra types to your compilation:

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.

InternalsVisisbleTo catch

There is an issue with the case shown before. Let’s say you have A.csproj targeting netstandard2.0 and A.Tests.csproj targeting net8.0 with InternalVisibleTo("A.Tests") inside A.csproj.

In this case, you won’t be able to compile A.Tests.csproj with an error about duplicate member definition, since the type like IsExternalInit would be available from two places - from A.csproj and from .net8.0 runtime library.

The solution is pretty simple: multitarget A.csproj and target both netstandard2.0 and net8.0.

And here I want to show all the language features from C# 12 down to C# 8 with their requirements and a link to a github issue that explains the feature.

C# 12 Features

Language Feature Requirements
ref-readonly parameters No extra requirements (1)
Collection expressions No extra requirements (2)
Interceptors InterceptsLocationAttribute (3)
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 (4)

(1) 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!

(2) System.Runtime.CompilerServices.CollectionBuilderAttribute is needed to support collection expression for custom types.

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

C# 11 Features

Language Feature Requirements
File-local types No extra requirements
ref fields a.k.a. low level struct enhancements .net7+
Required properties RequiredMemberAttribute, CompilerFeatureRequiredAttribute,
SetsRequiredMembersAttribute (1)
Static abstract members in interfaces .net7+
Numeric IntPtr No extra requirements
Unsigned right shift operator No extra requirements
utf8 string literals System.Memory nuget or .net2.1+
Pattern matching on ReadOnlySpan<char> System.Memory nuget package to get ReadOnlySpan itself.
Checked Operators No extra requirements
auto-default structs No extra requirements
Newlines in string interpolations No extra requirements
List patterns System.Index, System.Range(2)
Raw string literals No extra requirements
Cache delegates for static method group No extra requirements
nameof(parameter) No extra requirements
Relaxing Shift Operator No extra requirements
Generic attributes No extra requirements

(1) The full type names are System.Runtime.CompilerServices.RequiredMemberAttribute, System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute and System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute

(2) Some features are going to work only targeting net2.1 or netstandard2.1, for instance the following code requires System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray to be available:

int[] n = new int[]{ 1 };  
if (n is [1, .. var x, 2])  
{  
}

C# 10 Features

Language Feature Requirements
Record structs No extra requirements
Global using directives No extra requirements
Improved Definite Assignment No extra requirements
Constant Interpolated Strings No extra requirements
Extended Property Patterns No extra requirements
Sealed record ToString No extra requirements
Source generators V2 API No extra requirements
Mix declarations and variables in deconstruction No extra requirements
AsyncMethodBuilder override AsyncMethodBuilderAttribute (1)
Enhanced #line directives No extra requirements
Lambda improvements No extra requirements
Interpolated string improvements InterpolatedStringHandler, InterpolatedStringHandlerArgument (2)
File-scoped namespaces No extra requirements
Paremeterless struct constructors No extra requirements
CallerArgumentExpression CallerArgumentExpressionAttribute

(1) The full type name is System.Runtime.CompilerServices.AsyncMethodBuilderAttribute. (2) The full type names are System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute and System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute.

C# 9 Features

Language Feature Requirements
Target-typed new No extra requirements
Skip local init SkipLocalsInitAttribute
Lambda discard parameters No extra requirements
Native ints No extra requirements
Attributes on local functions No extra requirements
Function pointers No extra requirements
Pattern matching improvements No extra requirements
Static lambdas No extra requirements
Records No extra requirements
Target-typed conditional No extra requirements
Covariant Returns .net5.0+
Extension GetEnumerator No extra requirements
Module initializers ModuleInitializerAttribute (1)
Extending partials No extra requirements
Top level statements No extra requirements

(1) The full type name is System.Runtime.CompilerServices.ModuleInitializerAttribute.

C# 8 Features

Language Feature Requirements
Default Interface Methods .net core 3.1+
Nullable reference types A bunch of nullability attributes (1)
Recursive Patterns No extra requirements
Async streams Microsoft.Bcl.AsyncInterfaces or .net core 3.1+
Enhanced usings No extra requirements
Ranges System.Index, System.Range
Null-coalescing assignment No extra requirements
Alternative interpolated strings pattern No extra requirements
stackalloc in nested contexts No extra requirements
Unmanaged generic structs No extra requirements
Static local functions No extra requirements
Readonly members No extra requirements

(1) There are a lot of attributes: - [AllowNull], [DisallowNull], [DoesNotReturn], [DoesNotReturnIf], [MaybeNull], [MaybeNullWhen], [MemberNotNull], [MemberNotNullWhen], [NotNull], [NotNullIfNotNull], [NotNullWhen]