What's New in C# 14
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:
| Feature | In one line | Learn more |
|---|---|---|
| Extension members | Extension properties, static extensions and operators in extension blocks | Extension Methods & Extension Members |
field keyword | Use the hidden backing field inside a property accessor | Properties & the field Keyword |
| Null-conditional assignment | customer?.Order = value; assigns only when not null | Nullable Types & Null Safety |
nameof on unbound generics | nameof(List<>) gives "List" | Below |
| Implicit span conversions | Arrays convert to spans in more places | Below |
| Modifiers on simple lambda parameters | (text, out result) => ... without writing types | Below |
| Partial constructors and events | Split a constructor or event between two files | Below |
| User-defined compound assignment | Overload += itself to change an object in place | Below |
| File-based app directives | #:package, #:property and friends at the top of a .cs file | Your First C# Program |
A quick recap of the big three
The three headline features work well together. Here they are in one small program:
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;
}
}[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:
Console.WriteLine(nameof(List<>));
Console.WriteLine(nameof(Dictionary<,>));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:
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;
}
}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.
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);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):
// 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:
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;
}
}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:
- Install the .NET 10 SDK and check it with
dotnet --list-sdks. - Change
<TargetFramework>tonet10.0in each project (or once inDirectory.Build.props). - Update NuGet packages that have .NET 10 versions, especially
Microsoft.*and EF Core packages. - Build and read every new warning. Warning
CS9258means a variable namedfieldinside a property accessor now refers to the backing field; rename your variable or write@field. A type namedextensionmust also be renamed or escaped as@extension. - 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.
- 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.0instead of raisingLangVersion. - A local variable called
fieldinside a property accessor. It now clashes with the keyword. - Expecting
+=to call youroperator +=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.0projects. - Headline features: extension members, the
fieldkeyword 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,refand 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
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
}
}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?
Unbound generic types are now allowed in nameof, and only the simple name is returned.
2. What happens in customer?.Address = LoadAddress(); when customer is null?
The right side runs only when the left side is not null.
3. How is a C# 14 user-defined += operator declared?
Compound assignment operators change the existing instance, so they are non-static and return nothing.
4. Which warning tells you that an identifier named field inside an accessor now binds to the backing field?
CS9258 flags code whose meaning changed because field became a keyword in accessors.
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
