C# 14 is the language version that shipped with .NET 10 in November 2025, and it is the default for every project that targets net10.0. Its new features range from long-requested extension members and the field keyword to smaller conveniences such as null-conditional assignment and nameof(List<>). This guide walks through every C# 14 feature with realistic examples, explains how the compiler implements each one, and flags the breaking changes to check before you upgrade. It ends with the status of C# 15, which is at release-candidate stage alongside .NET 11 as of September 2026.
What Is C# 14?#
Microsoft released C# 14 on November 11, 2025, together with .NET 10 and Visual Studio 2026. .NET 10 is a Long Term Support (LTS) release supported until November 14, 2028, so C# 14 is the version most teams will standardize on for years. You do not opt in explicitly: the compiler picks the default language version from the target framework, so net10.0 projects get C# 14, while net9.0 and net8.0 projects stay on C# 13 and C# 12.
The release has one headline feature, extension members, plus targeted improvements that remove long-standing friction:
| Feature | What it gives you | Who benefits most |
|---|---|---|
| Extension members | Extension properties, static extension members and extension operators | Library and framework authors |
The field keyword | Accessor logic without a hand-written backing field | Anyone writing properties, especially view models |
| Null-conditional assignment | a?.B = value and a?.B += value | Code that works with optional objects |
nameof with unbound generics | nameof(List<>) evaluates to "List" | Logging, diagnostics and generators |
| Implicit span conversions | Arrays and strings convert to spans in more places | Performance-sensitive code and API designers |
| Modifiers on simple lambda parameters | (text, out value) => ... without explicit types | Code built on delegates with ref or out parameters |
| Partial constructors and events | Separate defining and implementing declarations | Source generator authors |
| User-defined compound assignment | public void operator +=(T value) for in-place updates | Numeric, tensor and buffer types |
| File-based app directives | #:package, #:sdk, #:property, #:project and #! | Scripts and small tools |
The Roslyn feature status page also lists a quieter C# 14 change: expression trees may now contain calls that use optional or named arguments, which mostly matters to LINQ providers.
How C# 14 Features Work Under the Hood#
The most useful mental model is that nearly all of C# 14 is compiler work. Each feature lowers to ordinary IL and metadata, so none of them needs new runtime capabilities. Knowing the lowering helps you predict behavior and binary compatibility:
- Extension members compile to static methods on the enclosing static class. Instance extension methods produce exactly the same metadata as classic
this-parameter extension methods, extension properties become static accessor methods such asget_IsWeekend, and extension operators become staticop_methods. - The
fieldkeyword makes the compiler synthesize a private backing field, just as it does for auto-properties. - Null-conditional assignment becomes a null check around an ordinary assignment, evaluating the receiver once.
- Compound assignment operators are instance methods with reserved metadata names such as
op_AdditionAssignmentandop_IncrementAssignment. - Implicit span conversions are new built-in conversions, but the generated code still calls the conversion members that
Span<T>andReadOnlySpan<T>already provide.
Language version and target framework are therefore related but distinct. You can force <LangVersion>14</LangVersion> onto an older target, but Microsoft documents that using a language version newer than your target framework's default is unsupported. The span changes show why: C# 14 overload resolution assumes the .NET 10 library surface. The evolution of C# guide covers the LangVersion rules in more detail.
Getting Started with C# 14#
Install the .NET 10 SDK (or Visual Studio 2026) and target net10.0. No extra property is required because C# 14 is the default:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>The quickest way to experiment is a file-based app, which .NET 10 introduced alongside C# 14. Save the following as demo.cs and run it with dotnet run demo.cs; no project file is needed:
Order? current = new() { Customer = "Contoso" };
Order? missing = null;
current?.Note = " gift wrap "; // null-conditional assignment
missing?.Note = "never written"; // skipped: no NullReferenceException
Console.WriteLine(nameof(List<>)); // List
Console.WriteLine(current.Customer.IsBlank); // False (extension property)
Console.WriteLine($"Note: '{current.Note}'"); // Note: 'gift wrap'
public sealed class Order
{
public required string Customer { get; init; }
// field keyword: trimming logic without a declared backing field
public string? Note { get; set => field = value?.Trim(); }
}
public static class TextExtensions
{
extension(string? text)
{
public bool IsBlank => string.IsNullOrWhiteSpace(text);
}
}That short file uses four C# 14 features; the sections below cover every feature in depth.
Extension Members: Properties, Static Members and Operators#
Classic extension methods, introduced in C# 3 for LINQ, could only add instance-style methods. C# 14 generalizes the idea with an extension block declared inside a top-level, non-generic static class. The block names the receiver once, and every member inside it can use that receiver. You can declare instance and static methods, instance and static properties, and user-defined operators.
namespace Contoso.Scheduling;
public static class DateOnlyExtensions
{
// Instance extension members: the receiver is named, so members can use it.
extension(DateOnly date)
{
public bool IsWeekend => date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
public DateOnly NextBusinessDay()
{
var next = date.AddDays(1);
while (next.IsWeekend) next = next.AddDays(1);
return next;
}
}
// Static extension members: the receiver is only a type.
extension(DateOnly)
{
public static DateOnly TodayUtc => DateOnly.FromDateTime(DateTime.UtcNow);
public static int operator -(DateOnly later, DateOnly earlier) =>
later.DayNumber - earlier.DayNumber;
}
}Callers use these members as if the type declared them: invoice.DueDate.IsWeekend, DateOnly.TodayUtc and dueDate - DateOnly.TodayUtc. The receiver can be generic, which is where the feature shines for library authors. Put a type parameter on the extension declaration when the receiver uses it, and on the individual member when only that member needs it:
public static class SequenceExtensions
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any();
public IEnumerable<T> EveryNth(int step)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(step);
return source.Where((_, index) => index % step == 0);
}
public Dictionary<TKey, int> CountByKey<TKey>(Func<T, TKey> keySelector)
where TKey : notnull =>
source.CountBy(keySelector).ToDictionary();
}
}Three rules keep extension members predictable. First, they never override real members: the compiler considers extensions only after ordinary member lookup fails, so a type that later adds its own IsEmpty silently wins. Second, extension blocks cannot add state. There are no extension fields or events, and extension properties cannot have init accessors, so attached state still needs something like ConditionalWeakTable. Third, the members are static methods underneath, so you can call an implementation directly, for example SequenceExtensions.EveryNth(items, 2), to resolve an ambiguity between two static classes.
Because new-style instance extension methods compile to the same IL as this-parameter methods, you can migrate one method at a time or leave existing ones alone, and both forms can share a static class. Extension indexers arrive in C# 15.
The field Keyword: Field-Backed Properties#
Before C# 14, adding one line of logic to an auto-property forced you to declare a private field and write both accessors by hand. The contextual keyword field refers to a compiler-synthesized backing field, and you can mix an auto accessor such as get; with an accessor that has a body. The feature previewed in C# 13 and is final in C# 14.
using System.ComponentModel;
using System.Runtime.CompilerServices;
public sealed class CustomerViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
// The initializer writes the backing field directly; the setter is not called.
public string Name
{
get;
set => SetField(ref field, value?.Trim() ?? string.Empty);
} = string.Empty;
public decimal DiscountRate
{
get;
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, 0.5m);
SetField(ref field, value);
}
}
// Lazy initialization with no separate field declaration.
public IReadOnlyList<string> Segments => field ??= LoadSegments();
private bool SetField<T>(ref T storage, T value, [CallerMemberName] string? name = null)
{
if (EqualityComparer<T>.Default.Equals(storage, value)) return false;
storage = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
return true;
}
private static IReadOnlyList<string> LoadSegments() => ["retail", "wholesale"];
}Several details make this more than syntax sugar. A property initializer assigns the backing field directly, whereas assigning the property in a constructor calls the setter, so you can set a default without raising change notifications. Field-targeted attributes such as [field: NonSerialized] work on any property that uses the backing field. Nullable analysis understands the lazy Segments pattern: it infers that the hidden field may be null while the property is not, so you get no spurious warnings.
Because the backing field is visible only inside the property, other members cannot bypass validation. When several members genuinely need the storage, an explicit field is still the right tool.
Null-Conditional Assignment#
The ?. and ?[] operators can now appear on the left side of an assignment or compound assignment. The right side is evaluated only when the receiver is not null, which is exactly the semantics of the if statement it replaces:
public sealed class CheckoutService(TimeProvider clock, IPricingService pricing)
{
public void Touch(Customer? customer, Cart? cart, Dictionary<string, int>? counters)
{
customer?.LastSeenUtc = clock.GetUtcNow(); // skipped when customer is null
cart?.Quote = pricing.GetQuote("EU"); // GetQuote runs only if cart is not null
cart?.ItemCount += 1; // compound assignment works
counters?["checkout"] = 1; // element access works
customer?.ProfileChanged += OnProfileChanged; // event subscription works
// customer?.VisitCount++; // error: ++ and -- are not allowed
}
private static void OnProfileChanged(object? sender, EventArgs e) { }
}Increment and decrement operators are deliberately excluded, as are deconstruction targets and ref assignment. The feature also does not help with value-type receivers, because a nullable struct's Value is not a variable you can assign into. Use null-conditional assignment where a missing receiver is a legitimate no-op, such as an optional UI element or collaborator. When null signals a bug, a guard that throws is still better.
nameof with Unbound Generic Types#
nameof previously required a closed generic type, so developers wrote nameof(List<int>) and picked an arbitrary type argument. That was more than cosmetic: a constraint change on the type parameter could break the nameof expression. C# 14 accepts unbound generic types, including member chains:
Console.WriteLine(nameof(List<>)); // List
Console.WriteLine(nameof(Dictionary<,>)); // Dictionary
Console.WriteLine(nameof(List<>.Count)); // Count
throw new InvalidOperationException(
$"No {nameof(IRepository<>)} implementation is registered for {typeof(Invoice).Name}.");Partially unbound forms such as Dictionary<int,> and nested unbound arguments such as List<List<>> remain invalid, matching the rules for typeof.
Implicit Span Conversions (First-Class Spans)#
Span<T> and ReadOnlySpan<T> have been central to high-performance .NET since C# 7.2, but the language treated their conversions as ordinary user-defined operators. Those conversions were ignored for extension method receivers and generic type inference, so the libraries had to ship duplicate array and span overloads. C# 14 adds built-in implicit span conversions: a single-dimensional array converts to Span<T> and ReadOnlySpan<T>, Span<T> converts to ReadOnlySpan<T>, ReadOnlySpan<T> converts covariantly to a span of a base reference type, and string converts to ReadOnlySpan<char>.
using System.Numerics;
int[] orderValues = [120, 75, 310, 45];
// Generic inference now sees through the array-to-span conversion (C# 13: CS0411).
int total = Sum(orderValues);
// Span-based extension methods apply directly to arrays.
int firstLarge = orderValues.IndexOfAny(310, 450);
// Covariant conversion: string[] to ReadOnlySpan<object>.
string[] names = ["Ada", "Grace"];
ReadOnlySpan<object> items = names;
Console.WriteLine($"{total} {firstLarge} {items.Length}");
static T Sum<T>(ReadOnlySpan<T> values) where T : INumber<T>
{
var sum = T.Zero;
foreach (var value in values) sum += value;
return sum;
}For API designers this means one ReadOnlySpan<T> overload can now serve arrays, spans and strings, which is the direction the .NET libraries are taking. For consumers it mostly means less .AsSpan() noise and more calls landing on vectorized MemoryExtensions implementations. The same change is the main source of C# 14 breaking changes, covered below. For background on spans, see Span and Memory explained.
Lambda Parameter Modifiers Without Types#
Lambdas could always infer parameter types, but adding a modifier such as out forced you to spell out every type. C# 14 lets you write scoped, ref, in, out or ref readonly on implicitly typed parameters:
// C# 13 required: (string text, out int value) => int.TryParse(text, out value)
TryParser<int> parseInt = (text, out value) => int.TryParse(text, out value);
TryParser<Guid> parseGuid = (text, out value) => Guid.TryParse(text, out value);
List<int> ids = ParseAll(["42", "oops", "7"], parseInt); // [42, 7]
static List<T> ParseAll<T>(IEnumerable<string> inputs, TryParser<T> parser)
{
var results = new List<T>();
foreach (var input in inputs)
if (parser(input, out var value)) results.Add(value);
return results;
}
delegate bool TryParser<T>(string text, out T value);The params modifier is the exception: it still requires an explicitly typed parameter list.
Partial Constructors and Partial Events#
C# 13 added partial properties and indexers; C# 14 completes the set with instance constructors and events. Each partial member has exactly one defining declaration and one implementing declaration. Only the implementing constructor may have a this() or base() initializer, and the implementing event must provide add and remove accessors while the defining declaration looks like a field-like event.
// Written by you: the shape of the type.
public partial class DeviceClient
{
public partial DeviceClient(string deviceId);
public partial event EventHandler<string>? StatusChanged;
}
// Emitted by a source generator: the implementation.
public partial class DeviceClient
{
private readonly string _deviceId;
private EventHandler<string>? _statusChanged;
public partial DeviceClient(string deviceId)
{
ArgumentException.ThrowIfNullOrEmpty(deviceId);
_deviceId = deviceId;
}
public partial event EventHandler<string>? StatusChanged
{
add => _statusChanged += value;
remove => _statusChanged -= value;
}
private void RaiseStatusChanged(string status) => _statusChanged?.Invoke(this, status);
}The primary audience is generator authors: weak-event libraries, interop binding generators and dependency injection helpers can now let users declare a constructor or event and supply the body themselves. If you write generators, the Roslyn source generators guide shows how to emit the implementing half.
User-Defined Compound Assignment and Increment Operators#
Until C# 14, x += y on a user-defined type always meant x = x + y. For large mutable structures such as buffers, big integers and tensors, that forced the + operator to allocate and copy a new instance even though the caller was about to discard the old one. C# 14 lets a type declare compound assignment operators as instance methods that return void and mutate the target in place. It also allows instance ++ and -- operators with no parameters, and checked variants of the arithmetic forms.
public sealed class Histogram(int bucketCount)
{
private readonly long[] _buckets = new long[bucketCount];
public long this[int bucket] => _buckets[bucket];
public void Record(int bucket) => _buckets[bucket]++;
// Classic binary operator: must allocate a new instance.
public static Histogram operator +(Histogram left, Histogram right)
{
var result = new Histogram(left._buckets.Length);
result += left;
result += right;
return result;
}
// C# 14: in-place compound assignment, no allocation.
public void operator +=(Histogram other)
{
if (other._buckets.Length != _buckets.Length)
throw new ArgumentException("Bucket counts differ.", nameof(other));
for (var i = 0; i < _buckets.Length; i++)
_buckets[i] += other._buckets[i];
}
}With this type, dailyTotals += hourly updates dailyTotals without allocating. When a type declares no compound operator, the compiler falls back to the binary operator as before, so the feature is opt-in. Watch for aliasing: if two variables reference the same histogram, += now mutates the object both see, whereas x = x + y left the other reference untouched. Reserve in-place operators for clearly mutable containers.
File-Based App Directives and Other Small Changes#
C# 14 also defines how the compiler treats directives that begin with #: or #!: it ignores them, and the .NET SDK reads them to build file-based apps. The #! line makes a .cs file directly executable on Unix-like systems, while #:package, #:sdk, #:property and #:project replace the project file:
#!/usr/bin/env dotnet
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
var app = WebApplication.Create(args);
app.MapGet("/health", () => Results.Ok(new { Status = "Healthy" }));
app.Run();File-based apps enable native AOT by default, so the sample turns it off to keep reflection-based JSON for the anonymous type. SDK 10.0.300 and .NET 11 previews add an #:include directive for extra files. Finally, calls that omit optional arguments or use named arguments no longer fail to compile inside Expression<T> lambdas.
Best Practices#
- Upgrade the target framework, not just
LangVersion. C# 14 is designed and tested against the .NET 10 libraries. - Group extension members by receiver. One static class per extended concept, with one block per receiver shape, keeps discovery and disambiguation simple.
- Keep extension properties cheap and side-effect free. Callers read
order.IsOverdueas a property, so it should not hit a database. - Migrate classic extension methods only when you gain something. The IL is identical, so rewriting for style alone creates churn.
- Use
fieldfor small accessor logic such as validation, trimming, lazy creation and change notification. - Use null-conditional assignment only where null is a valid state. Silently skipping a write can hide a bug that a guard clause would surface.
- Design new APIs around
ReadOnlySpan<T>. One overload now covers arrays, spans and strings;OverloadResolutionPriorityAttributehelps when older overloads must stay. - Pair compound operators with binary operators, and document that
+=mutates in place.
Common Pitfalls#
- Existing identifiers named
field. Inside an accessor,fieldnow binds to the backing field. The compiler reports warning CS9258 where the meaning changed, and a local namedfieldin an accessor is an error. Use@fieldorthis.field, or rename. - New contextual keywords.
extensioncan no longer be a type name,scopedin a lambda parameter list is always a modifier, and a method returning a type namedpartialmust write@partial. - Span overloads in expression trees. Inside an
Expression<Func<int[], int, bool>>,array.Contains(value)now binds toMemoryExtensions.Contains. Interpreted expressions (Compile(preferInterpretation: true)) and some LINQ providers cannot handle spans and fail at run time. EF Core 10 handles this case; EF Core 8 and 9 did not receive the full fix. Cast toIEnumerable<T>, callAsEnumerable(), or invokeEnumerable.Containsexplicitly. - New ambiguities and covariant arrays. Helpers with both array and span overloads, such as some
Assert.Equaloverloads, can become ambiguous. If aSpan<T>overload receives a covariant array (astring[]in anobject[]variable), it throwsArrayTypeMismatchException, which is why the compiler generally prefersReadOnlySpan<T>. - C# 14 on older targets. Below .NET 10,
array.Reverse()can bind to the in-place,void-returningMemoryExtensions.Reverse. Onnet10.0, a dedicatedEnumerable.Reverseoverload for arrays avoids the problem. - Aliasing with in-place operators. Adding
+=to an existing mutable reference type changes behavior for callers that share references.
C# 14 vs C# 13 and C# 15: Which Version Should You Target?#
Language versions ride along with .NET releases, so the practical question is which runtime you can deploy. The table summarizes the versions most teams will encounter in late 2026:
| Version | Ships with | Support status (September 2026) | Headline features |
|---|---|---|---|
| C# 12 | .NET 8 (LTS) | Support ends November 10, 2026 | Primary constructors, collection expressions, inline arrays, alias any type |
| C# 13 | .NET 9 (STS) | Support ends November 10, 2026 | params collections, the Lock type, partial properties, allows ref struct |
| C# 14 | .NET 10 (LTS) | Supported until November 14, 2028 | Extension members, field, null-conditional assignment, first-class spans |
| C# 15 | .NET 11 (STS) | Release candidate; GA expected November 2026 | Union types, closed hierarchies, collection expression arguments, extension indexers |
Because .NET 8 and .NET 9 both leave support on November 10, 2026, moving to .NET 10 and C# 14 is the natural upgrade for most production code right now.
C# 15 is close. .NET 11 Release Candidate 1 shipped on September 8, 2026 with a go-live license and made C# 15 the default for projects targeting .NET 11. The RC1 release notes list the stabilized features: collection expression arguments ([with(capacity: 32), .. items]), union types declared with the union keyword, non-virtual static interface members, closed class hierarchies, labeled break and continue, and extension indexers. A memory-safety overhaul that ties unsafe to operations that actually dereference pointers remains a preview behind LangVersion preview plus a compiler feature flag. Because .NET 11 is a Standard Term Support release due in November 2026, LTS-focused teams will likely stay on C# 14 until .NET 12.
Frequently Asked Questions#
Do I need .NET 10 to use C# 14?#
Yes, officially. C# 14 is the default and supported language version for projects targeting .NET 10. Setting LangVersion to 14 on older frameworks may compile, but Microsoft does not support that combination, and the span-related overload resolution changes can bind calls differently than you expect.
Are C# 14 extension members compatible with existing extension methods?#
Yes. An instance extension method declared inside an extension block compiles to the same IL as a classic method with a this parameter, so the change is both source and binary compatible. You can mix both styles in one static class and migrate gradually, or not at all.
Can extension members add fields or events to a type?#
No. In C# 14 an extension block can contain methods, properties and operators, in instance and static forms, but no fields or events. Extension indexers are added in C# 15. Attached state still requires a side structure such as ConditionalWeakTable.
Will the field keyword break my existing code?#
Only in code that already uses the identifier field inside property accessors. The compiler warns with CS9258 where an existing member named field would now bind to the synthesized backing field, and reports an error for locals named field declared in accessors. Escaping the name as @field or using this.field fixes both cases.
When is C# 15 released, and what does it add?#
C# 15 ships with .NET 11, with general availability expected in November 2026. As of RC1 in September 2026 it is the default for .NET 11 projects and includes union types, closed class hierarchies, collection expression arguments, extension indexers and labeled break and continue, while the new memory-safety rules remain a preview.
Summary#
- C# 14 shipped with .NET 10 LTS in November 2025 and is the default for
net10.0. - Extension members add properties, static members and operators while staying IL-compatible with classic extension methods.
- The
fieldkeyword, null-conditional assignment,nameof(List<>)and typeless lambda modifiers remove everyday boilerplate. - First-class spans let one
ReadOnlySpan<T>overload serve arrays, spans and strings, but cause most upgrade breaks. - Partial constructors and events help source generators; compound assignment operators enable allocation-free updates.
- C# 15 is at release-candidate stage and arrives with .NET 11 in November 2026.