What's new in C# 14

11/3/2025

Each new .NET release introduces updates to C#, and .NET 10 is no exception. C# 14 includes a set of features designed to boost productivity, improve code readability, and enhance performance. Knowing what these new features are and when to use them will help you write more robust and maintainable code.

This guide covers every new C# 14 feature, explains its benefits, and provides practical code examples to help you navigate where and how you can use each one. These features include:

  1. The field keyword
  2. Extension blocks
  3. Partial constructors and events
  4. Null-conditional assignment operator
  5. Overload compound assignment operators
  6. Support for ref, in, and out parameters in lambdas without explicit parameter types
  7. Improvements to nameof for unbounded generics
  8. Implicit conversions for Span<T>

Let’s dive in!


1. Simplify Properties with the field Keyword

C# 14 introduces the field keyword, which allows you to access an auto-implemented property’s hidden backing field directly in your property’s get and set methods. This new feature frees you from creating manual backing fields for your properties.

Before C# 14

When working with simple data values in classes, auto-implemented properties simplify configuring read and write functionality for a value:

public string Name { get; set; }

The syntax is concise and readable. However, its simplicity falls apart as soon as you need additional logic inside the getter or setter. You’re no longer able to take advantage of auto-implemented properties and must manually define a private backing, hindering readability:

private string _name;

public string Name
{
    get => _name;
    set => _name = value ?? throw new ArgumentNullException(nameof(value));
}

The private _name field in the snippet above stores the property’s underlying value. When setting this property, the value is first validated before being assigned to the field.

The C# 14 Solution

With the new field keyword, you can inject additional logic into your getter and setter methods without having to create your own backing field. You retain the readability of auto-implemented properties while customizing their accessors.

public string Name
{
    // Use `field` to access the underlying field directly.
    get => field;
    set => field = value ?? throw new ArgumentNullException(nameof(value));
}

Notice how the snippet above includes validation in the set method. However, instead of using a manual backing field, it uses the field keyword.

The field keyword is contextual, meaning it's only available inside the get and set methods of your auto-implemented properties. You won’t be able to access a property’s field from other properties, constructors, or methods. Instead, you’ll have to use the property accessor itself.

A small caveat is that if your class already has a member called field, that member will conflict with the new keyword. It’s best to avoid using the keyword as a name altogether, since in such cases, the new field keyword will take precedence and shadow the class member.

Why Use It

  • Simplifies code: You don’t need extra fields for simple validation or notifications in properties.
  • Improves readability: The property doesn’t reference external fields, making it self-contained and easy to follow.
  • Keep Auto-Implemented Properties: You can add additional logic to your property without giving up the convenience that auto-implemented properties provide.

2. Organize Type Extensions with Extension Blocks

Extension blocks are a new feature in C# 14 that group all your extension members for a type into a single block. Besides organizing your extensions neatly, extension blocks also allow you to define more than just extension methods, such as extension properties, methods, static methods, and operator overloads for other types.

Before C# 14

Extension methods have been around since C# 3.0. They let you extend a class’s functionality using a static method and the this keyword:

public static class EnumerableExtensions
{
    public static T FirstOrFallback(this IEnumerable<T> enumerable, T fallback)
        => enumerable.FirstOrDefault() ?? fallback;
}

The extension method can be called on any compatible type like this:

var value = upcomingOrders.FirstOrFallback("No upcoming order");

Unfortunately, the old extension method syntax only allows you to write methods. Extending classes with other members, such as properties and operators, isn’t possible in C# 13 and earlier.

The C# 14 Solution

In C# 14, extension blocks give you the freedom to define any extension property, method, static method, or operator for a type without modifying the type directly. For example, the members in the snippet below extend the IEnumerable<T> type with a new property, method, static method, and operator overload:

public static class EnumerableExtensions
{
    extension<T>(IEnumerable<T> enumerable)
    {
        // Extension property
        public bool IsEmpty => !enumerable.Any();

        // Extension method
        public T FirstOrFallback(T fallback)
            => enumerable.FirstOrDefault() ?? fallback;
        
        // Extension static method
        public static IEnumerable<T> Range(int start, int count, Func<int, T> generator)
            => Enumerable.Range(start, count).Select((_, i) => generator(i));
        
        // Extension operator method
        public static IEnumerable<T> operator +(IEnumerable<T> first, IEnumerable<T> second)
        {
            foreach (var item in first)
                yield return item;
            foreach (var item in second)
                yield return item;
        }
    }
}

All of these extension members can be used as if they were defined on the underlying class itself.

// Check if list is empty using extension property
var emptyList = new List<string>();
if (emptyList.IsEmpty)
    // ...

// Get the first value or fallback to a default value using the extension method
var upcomingOrders = GetUpcomingOrders();
var value = upcomingOrders.FirstOrFallback("No upcoming order");

// Concatenate two enumerables into a single one using the operator extension
var listA = new[] {1, 2};
var listB = new[] {3, 4};
var combinedList = listA + listB;

// Generate a range of values using the static extension method
var generatedIds = IEnumerable<string>.Range(10000, 11000, i => $"MSC-{i}");

Be cautious of conflicting signatures when writing extension members. If your extension member matches the signature of a method on the underlying type, the type’s method will take precedence.

Why Use It

  • Code Organization: All extensions for a type are organized in a static class, which makes it easier to locate and navigate these extension members.
  • Flexibility: You’re not restricted to extension methods. Now you can write extension properties, methods, static methods, and operator overloads.

3. Reduce Boilerplate Code Using Partial Constructors and Events

Partial members let you split the definition and declaration of a class's members, which is especially useful when using source generators. C# 14 expands support for partial members to constructors and events, reducing manual boilerplate code.

Before C# 14

Partial types and members are not new to C#. However, only partial methods, properties, and indexers are supported before C# 14:

// Config.cs
public partial class Config
{
    public partial void ReloadConfig();
}


// Config.g.cs
public partial class Config
{
    public partial void ReloadConfig()
    {
        // Generated code to reload configuration
    }
}

Unfortunately, because constructors couldn’t be made partial, the only way to execute generated code in them is by calling a generated partial method. This is boilerplate code that you, as the developer, must remember to implement:

// Config.cs
public partial class Config
{
    public Config(string configPath)
    {
        // Developer explicitly calls the partial method to load the configuration
        Initialize(configPath);
    }
    
    // Declare partial method to be generated in Config.g.cs
    private partial void Initialize(string configPath);
}

// Config.g.cs
public partial class Config
{
    private partial void Initialize(string configPath)
    {
        // Generated code
    }
}

Another limitation in versions of C# before C# 14 prevents you from defining partial events. If an event accessor has to call any generated code, you must define the event accessor methods and call the generated method:

// Config.cs
public partial class Config
{
    private EventHandler? _configReloaded;
    public event EventHandler ConfigReloaded
    {
        add
        {
            // Call generated method
            OnConfigReloadedAdded(value);
            _configReloaded += value;
        }
        remove
        {
            // Call generated method
            OnConfigReloadedRemoved(value);
            _configReloaded -= value;
        }
    }
    
    private partial void OnConfigReloadedAdded(EventHandler handler);
    private partial void OnConfigReloadedRemoved(EventHandler handler);
}


// Config.g.cs
public partial class Config
{
    private partial void OnConfigReloadedAdded(EventHandler handler)
    {
        // Generated code...
    }

    private partial void OnConfigReloadedRemoved(EventHandler handler)
    {
        // Generated code...
    }
}

The C# 14 Solution

Starting with C# 14, partial constructors and events are now supported, reducing the boilerplate required when working with events and constructors in partial classes.

Using this new feature, you can simplify the snippets above to two lines of code:

// Config.cs
public partial class Config
{
    public partial event EventHandler ConfigReloaded;

    public partial Config(string configPath);
}


// Config.g.cs
public partial class Config
{
    // Implementation for partial event
    private EventHandler? _configReloaded;
    public partial event EventHandler ConfigReloaded
    {
        add
        {
            // Generated code
            _configReloaded += value;
        }
        remove
        {
            // Generated code
            _configReloaded -= value;
        }
    }
    
    // Implementation for partial constructor
    public partial Config(string configPath)
    {
        // Generated code
    }
}

Keep in mind that partial constructors cannot be static and must always have a declaration and an implementation specified. Similarly, partial events don’t support auto-implemented accessors, so you must always include an implementation that has the add and remove accessor.

Why Use It

  • Less Boilerplate Code: There’s no need to declare partial methods when calling generated code from a constructor or event accessor.
  • Automatic event management: Source generators can fully manage event accessors to implement patterns like weak events.

If your code makes use of source generators, these new partial members can clean up areas in your code.

4. Improve Readability with Null-Conditional Assignments

In C# 14, the null-conditional operator (?.) now works on the left-hand side of value assignments. This frees you from writing verbose conditional checks to ensure the variable you are assigning to is not null.

Before C# 14

A common approach in earlier C# versions is first to check whether an object is null before accessing or updating its members:

if (policy is not null)
{
    policy.CustomerId = customerId;
}

Without the if statement above, a NullReferenceException will be thrown if policy is null. Such a conditional statement is the only solution to this problem in C# 13 and earlier. However, C# 14 brings a welcome change to simplify this pattern.

The C# 14 Solution

Because the pattern above is so common in many codebases, C# 14 lets you use the existing null-conditional operator on the left-hand side of an assignment. You can simplify the snippet above to:

policy?.CustomerId = customerId;

If policy is null, then the assignment is skipped without throwing any exceptions. When skipping the assignment, the right-hand side of the assignment is not executed, preventing any side effects from occurring.

There are some limitations to this syntax. Firstly, it does not support certain unary operators, such as ++ and --. You’re also not able to use this syntax with destructors.

Why Use It

More Readable Null Checks: The syntax simplifies assignments to nested properties by allowing safer assignments without null checks. By keeping the null check and assignment in a single line, the code becomes cleaner and easier to understand.


5. Improve Performance with User-Defined Compound Assignment Operators

In C# 14, you can now implement operator overloads for compound assignment operators like +=, -=, etc. These additional operator overloads let you fine-tune the performance of compound assignments on your custom types.

Before C# 14

C# already supports overloading operators such as + and - on your own types:

public struct Money(string currency, decimal amount)
{
    public decimal Amount { get; set; } = amount;
    public string Currency { get; set; } = currency;

    public static Money operator +(Money a, Money b)
    {
        if (a.Currency != b.Currency)
            throw new InvalidOperationException("Cannot add amounts with different currencies.");
        
        return new Money(a.Currency, a.Amount + b.Amount);
    }

    public override string ToString() => $"{Amount} {Currency}";
}

C# then uses the operator overload for normal operations and compound assignments:

var cheque = new Money("USD", 200);
var savings = new Money("USD", 100);

// Uses the + operator overload
var total = cheque + savings;

// Also uses the + operator overload and creates a new Money instance
total += new Money("USD", 10);

While sharing the overload for binary and compound operations is convenient, it’s not the most efficient. Compound operations always assign the result back to the first operand. Since the current operator overloads always return new types, these operations cause unnecessary work, such as value copying. In earlier versions of C#, there’s no way to address these performance issues.

The C# 14 Solution

You can now provide explicit compound assignment operator overloads:

public struct Money(string currency, decimal amount)
{
    // …

    public void operator += (Money b)
    {
        if (this.Currency != b.Currency)
            throw new InvalidOperationException("Cannot add amounts with different currencies.");
        
        this.Amount += b.Amount;
    }
}

Notice how the += overload doesn’t create an additional Money object to store the result. Instead, it updates the current instance directly. This is more efficient as it prevents unnecessary memory allocations.

When implementing compound operator overloads for your own types, make sure the final result does not differ from the one produced by the binary operator. Otherwise, the differing results will confuse developers and lead to subtle bugs.

Why Use It

  • More efficient operators: Avoid unnecessary allocations or copying when performing operations on certain types
  • Transparent to consumer: Developers using your code get the improved performance without having to make any changes on their side.

6. Use Parameter Modifiers in Lambda Parameters Without Explicit Types

Lambdas now support ref, in, out, scoped, and ref readonly parameter modifiers in C# 14 without needing to specify parameter types. This small yet useful enhancement makes writing high-performance lambdas easier and improves their readability.

Before C# 14

When writing lambdas, C# tries to infer the parameter types so that they don’t have to be explicitly stated. However, if you want to use the ref, in, or out parameter modifier for any parameters in C# 13 or earlier, you must also include the parameter type:

delegate void ApplyDiscount(ref decimal money);

// Lambda with explicit parameter type marked as ref
ApplyDiscount apply10PercentDiscount = (ref decimal price) =>
{
    price *= 0.9m;
};

The C# 14 Solution

C# 14 brings a slight quality-of-life improvement that lets you omit the parameter type when using parameter modifiers, provided the type can be inferred:

// Lambda with omitted parameter type
ApplyDiscount apply20PercentDiscount = (ref price) => 
{
    price *= 0.8m;
}

Notice how you have to define a custom delegate to use these parameter modifiers. This custom delegate is necessary because the standard Action<> and Func<> delegates don’t support these modifiers.

Also, keep in mind that if you use the params modifier, you will still have to specify the parameter type.

Why Use It

Readability: You can now write performance-critical lambda definitions more concisely, improving readability. The syntax for these lambdas is also more consistent with standard lambda functions.


7. Improve Diagnostics with nameof for Unbound Generics

C# 14 now lets you use the nameof operator on unbounded generics. These are generic types that don’t have a specified type argument, like List<>.

Before C# 14

In C# 13 and prior, the nameof operator only supports bounded generics. If you want to get the name of a generic type, you have to specify a random type parameter for it to compile and run:

// Using string as an arbitrary type parameter
Console.WriteLine(nameof(List<string>));
// Output: List

The snippet above illustrates how this approach makes logging and diagnostics cumbersome. You have to specify a type parameter for the generic type, even though it’s never output by nameof.

The C# 14 Solution

This quality-of-life improvement in C# 14 now lets you get the name of the type without a type parameter:

// No type parameter (unbounded generic)
Console.WriteLine(nameof(List<>));
// Output: List

If you want more information about the generic type, use the typeof operator, which provides more detailed information about the type, such as its assembly, namespace, and properties.

Why Use It

Readability: Specifying an arbitrary type parameter to obtain the name of a generic class can be confusing. Being able to retrieve the name of a generic class without specifying a type can greatly enhance readability.


8. Streamline Span<T> Usage with Implicit Conversions

The Span<T> and ReadOnlySpan<T> were introduced in C# to manage collections in a memory-efficient manner. In a push to improve the developer experience when working with these types in C#, the latest version comes with additional implicit operators that can convert various types into Span<T> and ReadOnlySpan<T>.

Before C# 14

To demonstrate how some of these new implicit conversions have been implemented, consider the method below, which accepts a ReadOnlySpan<decimal> and calculates the standard deviation:

public T CalculateStandardDeviation<T>(ReadOnlySpan<T> values) where T : INumber<T>
{
    // Logic for calculation
}

In versions before C# 14, you need to explicitly specify the type of the elements in the array of values when calling the method, like this:

// Array is converted to ReadOnlySpan using explicit type
var sd = CalculateStandardDeviation<int>(new[] { 3, 5, 3 });

The int type hint tells the compiler to convert this array of integers into a span of them. Failure to do so results in a compile-time error:

Screenshot of the compile-time error shown in C# 13 and earlier.

The C# 14 Solution

With C# 14, new implicit converters make it possible to infer the type to use when converting to ReadOnlySpan<T>, meaning you can omit the explicit type completely:

// Array is implicitly converted to ReadOnlySpan<int>
var sd = CalculateStandardDeviation(new[] { 3, 5, 3 });

There may still be scenarios where the implicit conversions won’t pick up the correct underlying type. However, you’re far less likely to run into these issues in C# 14.

Why Use It

  • Make Span<> First-Class: The goal of these new implicit conversions is to provide first-class support for Span<> and its related types in C#, allowing developers to use them more naturally without needing explicit conversions.
  • Readability: Explicit type parameters often make a method call seem verbose. By inferring these types, the code becomes clearer and easier to understand.

Conclusion

C# 14 introduces a range of quality-of-life updates and performance improvements. Most of the features aim to improve developer productivity by reducing boilerplate code. However, adopting a feature like user-defined compound assignments can also lead to real performance gains.

Implementing these new features in your codebase isn’t hard. First, update your projects to use .NET 10. This step might involve a few breaking changes, but they’re generally easy to fix. Once that’s done, you can start using these new features in your code.

If you’re interested in learning more about C# and some of the features highlighted in this article, be sure to check out the courses below.

About the Author

Ivan Kahl

Ivan Kahl

Ivan Kahl is a Software Engineer and Technical Writer specializing in .NET. His expertise also includes in-depth exposure to web technologies like React and cloud platforms such as AWS and Azure. He’s designed and built complex systems, often with Domain-Driven Design and Event-Driven Architecture. He has written on various technologies for global companies as a freelancer and runs a personal blog sharing his learnings.

View all courses by Ivan Kahl

What's New

What's new in C# 14
blog

What's new in C# 14

This guide covers every new C# 14 feature, explains its benefits, and provides practical code examples to help you navigate how you can use them.

Learn More
Let's Build It: AI Chatbot with RAG in .NET Using Your Data
course

Let's Build It: AI Chatbot with RAG in .NET Using Your Data

Build a Retrieval-Augmented Generation (RAG) chatbot that can answer questions using your data.

Learn More
Working with Large Language Models
tutorial

Working with Large Language Models

Learn how to work with Large Language Models (LLMs). Understand the fundamentals of how GPT works, the transformer architecture, and master prompt engineering techniques to build AI agents.

Learn More
From Zero to Hero: SignalR in .NET
course

From Zero to Hero: SignalR in .NET

Enable enterprise-grade real-time communication for your web apps with SignalR.

Learn More
Deep Dive: Solution Architecture
course

Deep Dive: Solution Architecture

Master solution architecture and turn business needs into scalable, maintainable systems.

Learn More
Migrating: ASP.NET Web APIs to ASP.NET Core
course

Migrating: ASP.NET Web APIs to ASP.NET Core

A step-by-step process to migrate ASP.NET Web APIs from .NET Framework to ASP.NET Core.

Learn More
Getting Started: Caching in .NET
course

Getting Started: Caching in .NET

Let's make the hardest thing in programming easy for .NET software engineers.

Learn More
From Zero to Hero: Testing with xUnit in C#
course

From Zero to Hero: Testing with xUnit in C#

Learn how to test any codebase in .NET with the latest version of xUnit, the industry-standard testing library.

Learn More
Create a ChatGPT Console AI Chatbot in C#
blog

Create a ChatGPT Console AI Chatbot in C#

This walkthrough is your hands-on entry point to create a basic C# console application that talks to ChatGPT using the OpenAI API.

Learn More