EthicsComputerCourses
Join training +91 90532 10052

What's New in C# 14

C# · Lesson 32 of 43 Intermediate 8 min read Updated

C# 14 shipped with .NET 10 in November 2025, and it is the version this course teaches. Several of its features already appeared in earlier lessons. This lesson collects all of them in one place, explains the smaller ones you have not met yet, and shows how to move an existing project to C# 14 safely.

Knowing what changed in each version is useful in interviews and when you read code: if you see field inside a property or extension(...) blocks, you know the code needs .NET 10.

The complete list

According to Microsoft's "What's new in C# 14" page, the release contains these features:

FeatureIn one lineLearn more
Extension membersExtension properties, static extensions and operators in extension blocksExtension Methods & Extension Members
field keywordUse the hidden backing field inside a property accessorProperties & the field Keyword
Null-conditional assignmentcustomer?.Order = value; assigns only when not nullNullable Types & Null Safety
nameof on unbound genericsnameof(List<>) gives "List"Below
Implicit span conversionsArrays convert to spans in more placesBelow
Modifiers on simple lambda parameters(text, out result) => ... without writing typesBelow
Partial constructors and eventsSplit a constructor or event between two filesBelow
User-defined compound assignmentOverload += itself to change an object in placeBelow
File-based app directives#:package, #:property and friends at the top of a .cs fileYour First C# Program

A quick recap of the big three

The three headline features work well together. Here they are in one small program:

C#
Order? today = new() { Customer = "  Ananya " };
Order? none = null;

today?.Customer = "  Ananya Rao  ";
none?.Customer = "Ghost"; // none is null, so nothing happens and nothing throws

Console.WriteLine($"[{today?.Customer}]");
Console.WriteLine(none is null);
Console.WriteLine(today!.Items.IsEmpty);

public class Order
{
    public string Customer
    {
        get;
        set => field = value.Trim();
    } = "";

    public List<string> Items { get; } = [];
}

public static class ListExtensions
{
    extension<T>(List<T> list)
    {
        public bool IsEmpty => list.Count == 0;
    }
}
Output
[Ananya Rao]
True
True

The field keyword lets the setter trim the value without declaring a private backing variable. ?. on the left of = assigns only when the object exists; if it is null, the right side is not even evaluated. And IsEmpty is an extension property, which classic extension methods could never offer.

nameof with unbound generic types

nameof returns the name of a symbol as a string, which keeps log messages and exceptions correct when you rename things. Before C# 14 you had to supply type arguments, as in nameof(List<int>). Now an unbound generic type (one with empty angle brackets) is allowed:

C#
Console.WriteLine(nameof(List<>));
Console.WriteLine(nameof(Dictionary<,>));
Output
List
Dictionary

Implicit span conversions

In Span<T>, Memory & Performance you saw that spans avoid copying. C# 14 makes Span<T> and ReadOnlySpan<T> "first class": the language itself knows that an array can become a span, and that a Span<T> can become a ReadOnlySpan<T>. The most visible effect is that an array can now call an extension method written for spans:

C#
int[] lastOver = [4, 6, 1, 0, 6, 2];
Console.WriteLine(lastOver.CountSixes());

static class ScoreExtensions
{
    public static int CountSixes(this ReadOnlySpan<int> balls)
    {
        int sixes = 0;
        foreach (int runs in balls)
        {
            if (runs == 6) sixes++;
        }
        return sixes;
    }
}
Output
2

In C# 13 that call did not compile, because the conversion from an array to a span was an ordinary user-defined conversion, which extension method receivers never use. Library authors can now write one fast span version instead of separate array and span overloads.

Modifiers on simple lambda parameters

Lambda parameters can now carry out, ref, in, ref readonly or scoped without spelling out their types. Previously, adding one modifier forced you to write every type.

C#
TryParser<int> parseAge = (text, out result) => int.TryParse(text, out result);

if (parseAge("21", out int age))
{
    Console.WriteLine($"Next year: {age + 1}");
}

delegate bool TryParser<T>(string text, out T result);
Output
Next year: 22

The types of text and result come from the TryParser<int> delegate. The only exception is params, which still needs an explicit type.

Partial constructors and events

A partial class can be split across files, and C# already allowed partial methods and (since C# 13) partial properties. C# 14 adds partial constructors and events. One file has the defining declaration (the signature) and another has the implementing declaration (the body):

C#
// Invoice.cs: written by you
public partial class Invoice
{
    public partial Invoice(string number);
}

// Invoice.Generated.cs: typically written by a source generator
public partial class Invoice
{
    public string Number { get; }

    public partial Invoice(string number)
    {
        Number = number;
    }
}

You will rarely write both halves yourself. The feature exists for source generators, tools that write C# code during the build, such as the JSON and logging generators you will use in this course. Only the implementing half may call this(...) or base(...).

User-defined compound assignment

Until C# 14, a += b on your own type always meant a = a + b: the + operator had to build a new object. For big objects such as matrices or a shopping cart, changing the existing object is cheaper. Now you can declare += (and -=, *= and so on) directly as a public, non-static method that returns void:

C#
var cart = new Cart();
cart += 499m;
cart += 1299m;
Console.WriteLine($"{cart.Items} items, total Rs {cart.Total}");

public class Cart
{
    public int Items { get; private set; }
    public decimal Total { get; private set; }

    public void operator +=(decimal price)
    {
        Items++;
        Total += price;
    }
}
Output
2 items, total Rs 1798

Instance ++ and -- operators with no parameters were added in the same way. If a type declares only the classic static operator +, compound assignment keeps working as before.

Walkthrough: moving a project to C# 14

The C# version follows the target framework, so upgrading is mostly a .NET upgrade. A safe order:

  1. Install the .NET 10 SDK and check it with dotnet --list-sdks.
  2. Change <TargetFramework> to net10.0 in each project (or once in Directory.Build.props).
  3. Update NuGet packages that have .NET 10 versions, especially Microsoft.* and EF Core packages.
  4. Build and read every new warning. Warning CS9258 means a variable named field inside a property accessor now refers to the backing field; rename your variable or write @field. A type named extension must also be renamed or escaped as @extension.
  5. Run all tests. The new span conversions can make the compiler pick a different overload in rare cases, mostly in code that mixes arrays with span-based APIs.
  6. Adopt the new features gradually, where they make code clearer. Editors offer quick fixes, such as converting a backing field to field.

Common mistakes

  • Forcing C# 14 onto an older framework. C# 14 is supported only on .NET 10 and later; target net10.0 instead of raising LangVersion.
  • A local variable called field inside a property accessor. It now clashes with the keyword.
  • Expecting += to call your operator += on a property without a setter. Compound assignment on properties still goes through the getter, the static operator and the setter; the in-place form is used for variables, fields and similar locations.
  • Overusing extension properties. Put core behaviour on the type itself; extensions are for types you do not own.

Key points

  • C# 14 ships with .NET 10 (November 2025) and is the default for net10.0 projects.
  • Headline features: extension members, the field keyword and null-conditional assignment.
  • nameof(List<>) now works with unbound generic types.
  • First-class span conversions let arrays use span-based extension methods.
  • Lambdas can use out, ref and other modifiers without explicit parameter types.
  • Partial constructors and events support source generators; user-defined += changes objects in place.

Exercise

Write a Wallet class for a college canteen app with a Balance property whose setter (using the field keyword) refuses negative values by throwing ArgumentOutOfRangeException. Give it a user-defined -= operator that deducts a purchase. Then create a Wallet? variable that may be null and use null-conditional assignment to top it up to Rs 500 only when it exists. Spend Rs 120 and print the balance.

Show solution
C#
Wallet? meera = new();
meera?.Balance = 500m;

if (meera is not null)
{
    meera -= 120m;
    Console.WriteLine($"Balance: Rs {meera.Balance}");
}

public class Wallet
{
    public decimal Balance
    {
        get;
        set => field = value >= 0
            ? value
            : throw new ArgumentOutOfRangeException(nameof(value), "Balance cannot be negative.");
    }

    public void operator -=(decimal amount)
    {
        Balance -= amount; // goes through the setter, so overspending throws
    }
}
Output
Balance: Rs 380

meera -= 120m calls the instance -= operator on the existing object. Inside it, Balance -= amount is ordinary decimal arithmetic on the property, and the field-based setter blocks any purchase that would make the balance negative.

Quick Quiz

1. What does nameof(Dictionary<,>) return in C# 14?

2. What happens in customer?.Address = LoadAddress(); when customer is null?

3. How is a C# 14 user-defined += operator declared?

4. Which warning tells you that an identifier named field inside an accessor now binds to the backing field?

Learn C# with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call