C# Language Features vs. Target Frameworks
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:
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]