C# 14 Features: What’s New and Worth Using

C# 14 ships with .NET 10 and focuses on removing boilerplate rather than adding new paradigms. It introduces extension members, the field keyword, null-conditional assignment, first-class span conversions, and several smaller changes that tighten everyday code. Some features pay off immediately. Others matter only if you write libraries, source generators, or high-performance code. This guide covers the syntax, the runtime behavior, the breaking-change risks, and the refactors worth doing first.

Quick Takeaways

  • Extension members let you declare extension properties, operators, and static members inside an extension block, not only methods.
  • The field keyword gives custom property accessors direct access to the compiler-generated backing field. No private field declaration is needed.
  • Null-conditional assignment (obj?.Prop = value) removes the if (obj != null) guard from assignments.
  • Target .NET 10 (net10.0) with the .NET 10 SDK to use all of it.
Feature Primary Benefit Adoption Effort Best For
Extension members Extension properties and static members Medium Library authors
field keyword Less backing-field boilerplate Low Everyone
Null-conditional assignment Shorter null-safe writes Low Everyone
Implicit span conversions Fewer overloads, fewer allocations Low Performance code
nameof on unbound generics Cleaner logging and diagnostics Low Framework code
Lambda parameter modifiers No explicit types with out/ref Low Delegate-heavy code
Partial constructors/events Source generator friendliness Medium Tooling authors
User-defined compound assignment In-place operators, fewer allocations Medium Numeric and math types

Setting Up Your Environment

C# 14 is the default language version for projects that target .NET 10. Install the .NET 10 SDK, then set the target framework:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <!-- LangVersion defaults to 14 on net10.0. Set it explicitly only to pin it. -->
    <LangVersion>14</LangVersion>
  </PropertyGroup>
</Project>

Running dotnet build with the .NET 10 SDK now compiles every sample below. You can also use Visual Studio 2026, which supports C# 14 out of the box.

Extension Members: Beyond Extension Methods

Classic extension methods use a this modifier on the first parameter of a static method. That syntax cannot express extension properties, extension operators, or static extension members. C# 14 fixes this with the extension block.

Syntax Breakdown

An extension block declares the receiver once. Every member inside it uses that receiver.

public static class StringExtensions
{
    // Instance extension members: receiver is a named parameter
    extension(string? value)
    {
        // Extension property
        public bool IsNullOrEmpty => string.IsNullOrEmpty(value);

        // Extension method, no 'this' modifier needed
        public string Truncate(int maxLength) =>
            value is null || value.Length <= maxLength
                ? value ?? string.Empty
                : value[..maxLength];
    }

    // Static extension members: receiver has a type but no name
    extension(string)
    {
        public static string Empty2 => string.Empty;
        public static bool IsBlank(string? s) => string.IsNullOrWhiteSpace(s);
    }
}

Calling the members looks like calling built-in ones:

string? title = null;

Console.WriteLine(title.IsNullOrEmpty);       // True
Console.WriteLine("Hello, world".Truncate(5)); // Hello
Console.WriteLine(string.IsBlank("   "));      // True

The extension property compiles to a static accessor method, so there is no extra allocation or hidden state. The string.IsBlank(...) call resolves to the static extension block, which makes the type’s API surface feel larger without touching the type.

Generic Extension Blocks

The block takes its own type parameters. This replaces repeated <T> declarations on every method.

public static class EnumerableExtensions
{
    extension<TSource>(IEnumerable<TSource> source)
    {
        public bool IsEmpty => !source.Any();

        public IEnumerable<TSource> WhereNotNull() =>
            source.Where(item => item is not null);
    }
}
var names = new List<string?> { "Ada", null, "Linus" };

Console.WriteLine(names.IsEmpty);                 // False
Console.WriteLine(string.Join(", ", names.WhereNotNull())); // Ada, Linus

Both members share one TSource declaration. The old syntax required <TSource> on each method, so the new form removes repetition once a class has more than two or three extensions.

Extension Methods vs. Extension Members

Aspect Classic Extension Method extension Block
Declaration this on first parameter Receiver declared on the block
Properties Not supported Supported
Static members Not supported Supported
Binary compatibility Established Classic methods remain valid
Generic repetition Per method Once per block
Migration None required Optional, incremental

Existing extension methods keep working. You do not need a mass migration. Adopt the block syntax for new code and convert files when you add a property.

The field Keyword: Field-Backed Properties

Before C# 14, adding logic to one accessor forced you to declare a private field and write both accessors by hand. The field contextual keyword refers to the compiler-synthesized backing field directly.

Before and After

// C# 13: manual backing field required
public class Customer
{
    private string _name = string.Empty;

    public string Name
    {
        get => _name;
        set => _name = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
    }
}
// C# 14: use 'field' inside the accessor
public class Customer
{
    public string Name
    {
        get;
        set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
    }
}

The get; accessor stays auto-implemented. Only the set accessor has a body. The compiler generates the backing field and binds field to it.

Lazy Initialization and Change Notification

field shines in two patterns: lazy defaults and INotifyPropertyChanged.

public class Report
{
    // Lazy default without a second field
    public List<string> Lines => field ??= new List<string>();
}

public class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    public string Title
    {
        get;
        set
        {
            if (field == value) return;
            field = value;
            PropertyChanged?.Invoke(this, new(nameof(Title)));
        }
    } = string.Empty;
}

Report.Lines allocates the list on first read only. ViewModel.Title raises the event only when the value actually changes. The = string.Empty initializer assigns directly to the backing field.

Breaking-Change Warning

If your class already has a member named field, the keyword creates ambiguity. Inside an accessor, field now means the backing field. Disambiguate with @field or this.field:

public class Legacy
{
    private int field;   // existing member

    public int Value
    {
        get => this.field;       // refers to the member, not the keyword
        set => this.field = value;
    }
}

Search your codebase for identifiers named field before upgrading. The compiler warns about the ambiguity, so you will see it at build time.

Null-Conditional Assignment

The ?. and ?[] operators now work on the left side of an assignment. The right-hand side is evaluated only if the receiver is non-null.

public class Order { public decimal Total { get; set; } }
public class Customer { public Order? CurrentOrder { get; set; } }

Customer? customer = GetCustomer();

// C# 13
if (customer is not null)
{
    customer.CurrentOrder = CreateOrder();
}

// C# 14
customer?.CurrentOrder = CreateOrder();
customer?.CurrentOrder?.Total += 5m;

If customer is null, CreateOrder() never runs. This short-circuit behavior matters when the right-hand side has side effects or is expensive. Compound assignments (+=, -=) work. The increment and decrement operators (++, --) do not.

Implicit Span Conversions

C# 14 gives Span<T> and ReadOnlySpan<T> first-class language support. Arrays convert implicitly to both, and string converts to ReadOnlySpan<char>. Generic type inference and extension method resolution now see through those conversions.

static int CountVowels(ReadOnlySpan<char> text)
{
    int count = 0;
    foreach (char c in text)
    {
        if ("aeiouAEIOU".Contains(c)) count++;
    }
    return count;
}

string word = "Extension";
char[] buffer = ['s', 'p', 'a', 'n'];

Console.WriteLine(CountVowels(word));   // 3, no Substring, no allocation
Console.WriteLine(CountVowels(buffer)); // 1

Both calls pass without a manual .AsSpan(). The practical gain is fewer duplicate overloads (string, char[], ReadOnlySpan<char>) and fewer accidental allocations. Span-based extension methods also bind directly to arrays now.

nameof with Unbound Generic Types

nameof accepts an unbound generic type, so you no longer need a dummy type argument.

// C# 13
string a = nameof(List<int>);   // "List"

// C# 14
string b = nameof(List<>);      // "List"
string c = nameof(Dictionary<,>); // "Dictionary"

The output is identical. The new form removes an arbitrary type argument that implied meaning it did not have.

Lambda Parameters with Modifiers

You can now add ref, in, out, scoped, and ref readonly to lambda parameters without writing the types.

delegate bool TryParse<T>(string text, out T result);

// C# 13: types mandatory because of 'out'
TryParse<int> old = (string text, out int result) => int.TryParse(text, out result);

// C# 14: types inferred from the delegate
TryParse<int> parse = (text, out result) => int.TryParse(text, out result);

if (parse("42", out var value))
{
    Console.WriteLine(value); // 42
}

Inference comes from the target delegate type. The change removes noise in callback-heavy code and parser tables.

Partial Constructors and Partial Events

Partial members now include instance constructors and events. This helps source generators, which can declare the shape while you or another generator supply the body.

public partial class Widget
{
    // Defining declaration
    public partial Widget(int size);
}

public partial class Widget
{
    private readonly int _size;

    // Implementing declaration
    public partial Widget(int size)
    {
        _size = size;
    }
}

Only the implementing declaration can include a constructor initializer (this(...) or base(...)). Only one declaration may use primary constructor syntax. Most application developers will not write these by hand, but generator output will use them.

User-Defined Compound Assignment Operators

Before C# 14, a += b compiled as a = a + b, which allocated a new object for reference types. You can now define += and the other compound operators as instance methods that mutate in place.

public class Vector
{
    public double[] Values { get; }

    public Vector(params double[] values) => Values = values;

    // Allocating operator
    public static Vector operator +(Vector left, Vector right)
    {
        var result = new double[left.Values.Length];
        for (int i = 0; i < result.Length; i++)
            result[i] = left.Values[i] + right.Values[i];
        return new Vector(result);
    }

    // In-place compound operator: instance, void return
    public void operator +=(Vector other)
    {
        for (int i = 0; i < Values.Length; i++)
            Values[i] += other.Values[i];
    }
}
var a = new Vector(1, 2, 3);
var b = new Vector(4, 5, 6);

a += b; // mutates 'a' in place, zero new Vector allocations
Console.WriteLine(string.Join(", ", a.Values)); // 5, 7, 9

Use this for large buffers, matrices, and big-number types. Keep the in-place operator semantically consistent with the binary operator. Otherwise a = a + b and a += b produce different results and surprise callers.

Best Practices: Bad Code vs. Good Code

Backing Fields

// Bad: manual field exists only to hold one trim call
public class User
{
    private string _email = string.Empty;
    public string Email
    {
        get { return _email; }
        set { _email = value.Trim(); }
    }
}
// Good: field-backed property, same behavior
public class User
{
    public string Email
    {
        get;
        set => field = value.Trim();
    } = string.Empty;
}

Null-Safe Writes

// Bad: nested guards bury the intent
if (session != null)
{
    if (session.User != null)
    {
        session.User.LastSeen = DateTime.UtcNow;
    }
}
// Good: one line, same short-circuit semantics
session?.User?.LastSeen = DateTime.UtcNow;

Extension Helpers

// Bad: method call where a property reads naturally
public static bool GetIsEmpty<T>(this IEnumerable<T> source) => !source.Any();
// Good: extension property
extension<T>(IEnumerable<T> source)
{
    public bool IsEmpty => !source.Any();
}

Which Features Are Worth Adopting First?

Priority Feature Reason
1 field keyword Immediate boilerplate reduction, low risk
2 Null-conditional assignment Safer, shorter code in every project
3 Implicit span conversions Free performance on hot paths
4 Extension members Best for shared libraries and API design
5 Compound operators Only for allocation-sensitive numeric types
6 Partial constructors/events Only if you author source generators

Start with field and ?. assignment, since they carry the lowest migration risk. Introduce extension blocks when you next add an extension property. Treat compound operators as an optimization to apply after profiling shows allocation pressure.

Upgrade Checklist

  1. Install the .NET 10 SDK and set <TargetFramework>net10.0</TargetFramework>.
  2. Search for identifiers named field inside property accessors.
  3. Rebuild and review new warnings, especially around overload resolution with spans.
  4. Convert one class to field-backed properties and run your tests.
  5. Replace guard-then-assign blocks with ?. assignment.
  6. Re-run benchmarks on span-heavy code paths to confirm overload changes.

FAQ

Which .NET version supports C# 14?

.NET 10 supports C# 14. Install the .NET 10 SDK and target net10.0. C# 14 is the default language version for that target.

Does the field keyword break existing code?

Only if a class has a member named field and an accessor refers to it by simple name. The compiler warns about the ambiguity. Fix it with @field or this.field.

Do extension blocks replace classic extension methods?

No. Classic this-parameter extension methods remain fully supported. Extension blocks add properties, operators, and static members, and they reduce repetition when a class has many extensions.

Does null-conditional assignment work with ++ and --?

No. It works with simple assignment and compound assignments such as +=, but not increment or decrement operators.

Hot this week

The State of Robotics in 2026: 10 Biggest Developments

The 10 biggest robotics developments of 2026: whole-body VLA models, humanoid safety, ROS 2 Lyrical Luth, and Jetson Thor. Get the data and code.

EU Machinery Regulation 2027: What Robot Builders Need to Know

Building robots for the EU? Regulation (EU) 2023/1230 applies from 20 Jan 2027. Get the cybersecurity, AI, and CE marking checklist now.

ISO 10218:2025 Explained: The New Industrial Robot Safety Standard

ISO 10218:2025 rewrites industrial robot safety: Class I/II robots, built-in cobot limits, cybersecurity. Get the checklist and ROS2 code. Read now.

NVIDIA Jetson Orin Nano, AGX Orin, and Thor: Which One for Your Robot?

Jetson Orin Nano vs AGX Orin vs Thor: compare TOPS, memory bandwidth, power, and price to pick the right robot compute. Read the guide.

ROS 2 Distributions Explained: Humble, Jazzy, Kilted, and Lyrical (Which to Use)

Compare ROS 2 Humble, Jazzy, Kilted, and Lyrical by EOL date, platform support, and features. Pick the right distro for your robot. Read the guide.

Topics

The State of Robotics in 2026: 10 Biggest Developments

The 10 biggest robotics developments of 2026: whole-body VLA models, humanoid safety, ROS 2 Lyrical Luth, and Jetson Thor. Get the data and code.

EU Machinery Regulation 2027: What Robot Builders Need to Know

Building robots for the EU? Regulation (EU) 2023/1230 applies from 20 Jan 2027. Get the cybersecurity, AI, and CE marking checklist now.

ISO 10218:2025 Explained: The New Industrial Robot Safety Standard

ISO 10218:2025 rewrites industrial robot safety: Class I/II robots, built-in cobot limits, cybersecurity. Get the checklist and ROS2 code. Read now.

NVIDIA Jetson Orin Nano, AGX Orin, and Thor: Which One for Your Robot?

Jetson Orin Nano vs AGX Orin vs Thor: compare TOPS, memory bandwidth, power, and price to pick the right robot compute. Read the guide.

ROS 2 Distributions Explained: Humble, Jazzy, Kilted, and Lyrical (Which to Use)

Compare ROS 2 Humble, Jazzy, Kilted, and Lyrical by EOL date, platform support, and features. Pick the right distro for your robot. Read the guide.

Build a Low-Cost AI Robot Arm With SO-101 and LeRobot

Build an SO-101 robot arm under $250, calibrate it, record demos, and train an ACT policy with LeRobot. Follow the full guide and start building.

Robot Foundation Models: GR00T, pi, Gemini Robotics, and Open Alternatives Compared

Compare robot foundation models: NVIDIA GR00T, Physical Intelligence π, Gemini Robotics 2, and open VLAs. Get latency math, code, and a pick guide.

How Much Does a Humanoid Robot Cost? Prices, Subscriptions, and Hidden Costs

Humanoid robot cost in 2026: prices from $4,900, $499/mo subscriptions, and hidden fees. See the full TCO breakdown and compare models now.

Related Articles

Popular Categories