November 17th, 2025
likecelebrateheartcompelling7 reactions

Introducing C# 14

Bill Wagner
C# / .NET Principal Content developer

C# 14 ships with .NET 10. The highlight is new extension members, but there’s a lot more features that make your life as a developer more productive. And, we’ve added new features that enable some of the performance improvements you can experience in .NET 10. Read on for a tour of all the new features, and find links to dive deeper and start using these features today.

Extension members

Extension members are the headline feature of C# 14. The new syntax is fully compatible with existing extension methods. Extension members enables extension properties, extension operators, and static extension members.

The following code shows an example extension block. The extension block contains two instance extensions followed by two static extensions for the same type. The receiver name, source, is optional if the extension only contains static extensions.

public static class EnumerableExtensions
{
 // Instance-style extension members: 'source' is the receiver variable
 extension<TSource>(IEnumerable<TSource> source)
 {
  // Extension property
  public bool IsEmpty => !source.Any();

  // Extension method (body elided for brevity)
  public IEnumerable<TSource> Where(Func<TSource, bool> predicate)
  {
   // Implementation would filter 'source'
   throw new NotImplementedException();
  }

  // Static extension property
  public static IEnumerable<TSource> Identity => Enumerable.Empty<TSource>();

  // Static user-defined operator provided as an extension
  public static IEnumerable<TSource> operator +(
   IEnumerable<TSource> left,
   IEnumerable<TSource> right) => left.Concat(right);
 }
}

Usage examples:

int[] data = ...;
// access instance extension property:
if (data.IsEmpty) { /* ... */ }

// Access static extension operator +
var combined = data + [ 4, 5 ];

// Access static extension property:
var empty = IEnumerable<int>.Identity;

Because extension blocks are source and binary compatible with existing extension methods, you can migrate one method at a time. Dependent assemblies don’t need to be recompiled and continue to bind to the original symbol.

You can learn more and explore extension members in the C# Guide and the extension keyword article. You can also read all the details on the feature design in the Extensions proposal.

More productivity for you

This set of language features share a common goal: reduce the syntactic friction for everyday tasks so you can focus on domain logic instead of ceremony. They eliminate boilerplate, remove common conditional blocks, simplify lambda declarations, enhance partial types for source generators, and make nameof more expressive in generic scenarios. Individually each saves a few lines and more typing. Together they translate into cleaner code, fewer trivial identifiers, and code that communicates intent more cleanly.

The field keyword

Most properties start life as simple auto‑implemented properties. Later you discover you need small bits of logic — null coalescing, clamping, simple normalization, or raising a guard — on just one accessor. Before C# 14 that requirement forced you to convert to a fully hand‑written backing field pattern:

// Before
private string _message = "";
public string Message
{
 get => _message;
 init => _message = value 
           ?? throw new ArgumentNullException(nameof(value));
}

The contextual field keyword creates a middle step on that evolution path: keep the auto‑property terseness, inject minimal logic only where needed, and let the compiler synthesize and name the backing storage. You add just the accessor body that needs logic and refer to the compiler‑generated storage via field:

// After (C# 14)
public string Message
{
 get; // auto get
 init => field = value 
           ?? throw new ArgumentNullException(nameof(value));
}

It’s a bridge between auto‑implemented and fully hand‑written properties: start with public string Message { get; init; }, then when you need a quick guard, convert only the accessor that requires code and use field instead of introducing a private member and duplicating a trivial getter. This pattern scales when many properties each require a one‑line check—your class stays visually lightweight and diffs stay small. Another advantage of field is that it avoids creating a new named private field. All code in the type must use the property to access or modify the value of the property.

This feature was available as a preview in .NET 9. The field contextual keyword is now generally available in C# 14 (see what’s new).

Unbound generic types and nameof

Previously, to log or throw using just the generic type name you either hardcoded a string or used a closed constructed type:

// Before
var listTypeName = nameof(List<int>); // "List"
// or:
const string Expected = "List";

Now nameof accepts an unbound generic type. This feature removes the need to pick an arbitrary type argument just to retrieve the generic type’s name:

// After (C# 14)
var listTypeName = nameof(List<>); // "List"

This produces the generic type name once, without implying any specific instantiation. Learn more in the nameof operator reference.

Simple lambda parameters with modifiers

In earlier versions, parameter modifiers such as out in delegates required full type annotations on all parameters:

// Before
delegate bool TryParse<T>(string text, out T value);
TryParse<int> parse = (string text, out int result) => int.TryParse(text, out result);

Now you can keep the concise implicitly typed form while still using modifiers like out, ref, in, scoped on one or more parameters:

// After (C# 14)
TryParse<int> parse = (text, out result) => int.TryParse(text, out result);

The parameter types are still inferred, preserving the concise syntax of the lambda expression. Learn more in the C# language reference section on lambda expression parameter modifiers. It keeps lambdas terse while still exposing flow semantics (out, ref, in, scoped).

Null-conditional assignment

Guarded assignments previously required an explicit null check:

// Before
if (customer is not null)
{
 customer.Order = CreateOrder();
 customer.Total += CalculateIncrement();
}

Now you can assign (and use compound assignment) directly with null-conditional operators on the left side of the assignment. The right side is evaluated only when the receiver of the assignment isn’t null:

// After (C# 14)
customer?.Order = CreateOrder();
customer?.Total += CalculateIncrement();

That trims indentation and visually centers the important work. The feature integrates directly with the existing null-conditional operators so they can appear on the left side of an assignment. It evaluates the right-hand expression only when the receiver isn’t null, avoiding helper locals or duplicated checks. See null-conditional assignment and the feature specification.

Partial events and constructors

Large generated or source‑generated partial types can now spread event and constructor logic across files, enabling generators or different files to contribute cleanly:

public partial class Widget(int size, string name) // defining declaration of primary ctor
{
 public partial event EventHandler Changed; // declaring event declaration (field-like)
}

public partial class Widget
{
 public partial event EventHandler Changed // Defining declaration for event.
 {
  add => _changed += value;
  remove => _changed -= value;
 }

 private EventHandler? _changed;

 // Implementing declaration can add constructor body logic
 public Widget
 {
  Initialize();
 }
}

This separation enables new source generation scenarios (e.g., a generator supplies the defining members, user code supplies behavior, or vice-versa). It simplifies the manually authored logic. It remains more focused on the algorithms you write by hand.

See the programming guide for partial constructors and the partial member reference for syntax details.

More performance for your users

Many of the raw throughput wins you’ll see after upgrading to .NET 10 come from the runtime and BCL adopting new C# 14 capabilities. Core libraries already use these features so your apps often get faster even if you never write this syntax yourself. The .NET 10 performance improvements post highlights span-heavy parsing, UTF-8 processing, and numeric routines that benefit. Two language additions in particular unlock cleaner, faster library implementations: implicit span conversions and user-defined compound assignment.

Implicit span conversions

Span<T> / ReadOnlySpan<T> are central to allocation-free APIs. C# 14 adds implicit conversions among arrays, spans, and read-only spans so you write less ceremony and the JIT sees simpler call graphs. That translates into fewer temporary variables, fewer bounds checks, and more aggressive inlining in the framework (as described in the performance blog’s sections covering text and parsing micro-benchmarks).

Earlier C# versions required code like the following:

// Before
string line = ReadLine();
ReadOnlySpan<char> key = line.AsSpan(0, 5); // explicit AsSpan
ProcessKey(key);

int[] buffer = GetBuffer();
Span<int> head = new(buffer, 0, 8); // explicit Span ctor
Accumulate(head);

Now, you can write the following:

// After (C# 14)
string line = ReadLine();
ProcessKey(line[..5]);              // substring slice implicitly converts

int[] buffer = GetBuffer();
Accumulate(buffer[..8]);

Library authors exploit these conversions to remove helper locals and express slice intent inline. The benefits include fewer explicit AsSpan or constructor calls, clearer slicing intent that encourages span-friendly overloads, and framework optimizations that reduce allocations through broader zero-allocation paths. Learn more by reading the first-class span types spec.

User defined compound assignment

High-performance numeric and vector types often accumulate values in tight loops. Without a dedicated compound assignment operator, code either repeated the left hand reference or created intermediate temporaries through an ordinary binary operator—both patterns can inhibit certain JIT optimizations. C# 14 lets you declare a compound assignment operator (+=, -=, etc.) explicitly so the compiler dispatches directly to your implementation. Libraries taking advantage of this (for example, SIMD-friendly helpers referenced in the performance blog) to avoid extra temporaries and can expose more idiomatic APIs.

Instead of this:

// Before
BigVector sum = BigVector.Zero;
foreach (var v in values)
{
 sum = sum.Add(v); // intermediate result each iteration
}

After you provide a compound operator that can update the result in-place:

// After (C# 14)
BigVector sum = BigVector.Zero;
foreach (var v in values)
{
 sum += v; // calls user-defined operator += directly
}

Defining both the binary and compound operators:

public struct BigVector(float x, float y, float z)
{
 public float X { get; private set => value = field; } = x;
 public float Y { get; private set => value = field; } = y;
 public float Z { get; private set => value = field; } = z;

 public static BigVector operator +(BigVector l, BigVector r)
  => new(l.X + r.X, l.Y + r.Y, l.Z + r.Z);

 public void operator +=(BigVector r)
 {
  X += r.X;
  Y += r.Y;
  Z += r.Z;
 }
}

Details appear in the operator overloading article in the C# guide and the compound assignment spec. You should also consult the compiler breaking changes article for potential issues. You might encounter issues regarding Enumerable.Reverse.

Summary

That’s a quick tour of what we’ve delivered in C# 14: new extensions, a number of features that make you more productive, and enhancements that make your C# programs perform better. Download .NET 10 and try it on your apps. Participate in ongoing discussions to continue to make C# a great language choice for you.

Author

Bill Wagner
C# / .NET Principal Content developer

Bill Wagner writes the docs for https://docs.microsoft.com/dotnet/csharp. His team is responsible for all the .NET content on docs.microsoft.com. He's also a member of the C# standardization committee.

12 comments

Sort by :
  • David Roth · Edited

    C#14 is awesome – really cool features!
    Yet I am still disappointed that the left join proposal did not make it, despite the corresponding extension additions made in the BCL.

    See proposal: https://github.com/dotnet/csharplang/discussions/8892

    Proposal discussion:
    https://github.com/dotnet/csharplang/blob/main/meetings/2025/LDM-2025-02-12.md#left-right-join-in-query-expressions

    Quote: We will proceed with this proposal, hopefully in the C# 14 timeframe (barring any other time constraints).

    So please proceed with this for C#15 – left join keywords is one of the most top requested features. 🙏

  • Herb Fickes

    Please ask the Office team to support .Net 10 and latest C#. We’re stuck using .Net Framework 4.8 because they won’t move beyond it.

    There are all these great features and performance improvements and we can’t use most of them. We have back ported some .Net 5/6 features back into our codebase but there is lots of great stuff that Word forces us to live without.

  • Pavel Kotrč

    In the last example, the setters should be written as

    field = value

    , not vice versa.

  • Richard Deeming

    One thing that seems to be missing from the new extension blocks documentation is a description of how to apply XML doc comments to them.

    Taking the Where method from this post as an example: if I add the param tag for source to the method itself, VS complains that the parameter does not exist. Similarly if I add any reference to source in a paramref tag.

    Adding the param tag to the extesion block itself seems to satisfy VS. But then tools like SHFB report that the parameter documentation is missing.

    None of the examples I've see include any XML doc comments,...

    Read more
      • Richard Deeming · Edited

        Thanks for that. But I think there may be an issue with how VS2026 is generating the XML from the comments.

        Given a simple example:

        <code>

        the generated XML contains:

        <code>

        The comments on the extension block itself are included, but are not referenced anywhere from the method documentation.

        Surely, either the comments from the extension block should be merged with the comments from the method, or the member documentation should have an additional inheritdoc reference to the extension block documentation?

        Or is there some hidden heuristic that should be used to link from the method documentation to the containing extension block documentation?

        Read more
  • Arash Laylazi

    It looks like the thumbnail image for this post is incorrect.

    
    

    It displays an image illustrating the concept of “Build, Test, Deploy”.
    I think the operator duplicated another post and forgot to update all the meta tags!

  • Gauthier M.

    I realy like the field keyword and the null-conditional assignment.
    It will produce more compact and readable code.
    Thanks for all your work on C#

  • Uwe Keim

    I’m looking forward for a null-aware “await” statement.