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:

FeatureWhat it gives youWho benefits most
Extension membersExtension properties, static extension members and extension operatorsLibrary and framework authors
The field keywordAccessor logic without a hand-written backing fieldAnyone writing properties, especially view models
Null-conditional assignmenta?.B = value and a?.B += valueCode that works with optional objects
nameof with unbound genericsnameof(List<>) evaluates to "List"Logging, diagnostics and generators
Implicit span conversionsArrays and strings convert to spans in more placesPerformance-sensitive code and API designers
Modifiers on simple lambda parameters(text, out value) => ... without explicit typesCode built on delegates with ref or out parameters
Partial constructors and eventsSeparate defining and implementing declarationsSource generator authors
User-defined compound assignmentpublic void operator +=(T value) for in-place updatesNumeric, 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 as get_IsWeekend, and extension operators become static op_ methods.
  • The field keyword 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_AdditionAssignment and op_IncrementAssignment.
  • Implicit span conversions are new built-in conversions, but the generated code still calls the conversion members that Span<T> and ReadOnlySpan<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:

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

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

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

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

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

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

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

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

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

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

C#
#!/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.IsOverdue as 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 field for 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; OverloadResolutionPriorityAttribute helps 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, field now binds to the backing field. The compiler reports warning CS9258 where the meaning changed, and a local named field in an accessor is an error. Use @field or this.field, or rename.
  • New contextual keywords. extension can no longer be a type name, scoped in a lambda parameter list is always a modifier, and a method returning a type named partial must write @partial.
  • Span overloads in expression trees. Inside an Expression<Func<int[], int, bool>>, array.Contains(value) now binds to MemoryExtensions.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 to IEnumerable<T>, call AsEnumerable(), or invoke Enumerable.Contains explicitly.
  • New ambiguities and covariant arrays. Helpers with both array and span overloads, such as some Assert.Equal overloads, can become ambiguous. If a Span<T> overload receives a covariant array (a string[] in an object[] variable), it throws ArrayTypeMismatchException, which is why the compiler generally prefers ReadOnlySpan<T>.
  • C# 14 on older targets. Below .NET 10, array.Reverse() can bind to the in-place, void-returning MemoryExtensions.Reverse. On net10.0, a dedicated Enumerable.Reverse overload 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:

VersionShips withSupport status (September 2026)Headline features
C# 12.NET 8 (LTS)Support ends November 10, 2026Primary constructors, collection expressions, inline arrays, alias any type
C# 13.NET 9 (STS)Support ends November 10, 2026params collections, the Lock type, partial properties, allows ref struct
C# 14.NET 10 (LTS)Supported until November 14, 2028Extension members, field, null-conditional assignment, first-class spans
C# 15.NET 11 (STS)Release candidate; GA expected November 2026Union 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 field keyword, 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.

Further Reading#