- Home
- .NET tutorials
- Are C# 14's new features worth updating your app to .NET 10?
Are C# 14's new features worth updating your app to .NET 10?
Published: Tuesday 11 November 2025
C# 14 has officially landed - and with it, a range of new features designed to make your code cleaner, safer, and more expressive. But the big question is: should you update your application to .NET 10 to take advantage of them?
Let's explore what's new in C# 14, see some examples in action, and help you decide whether the update is worth it right now.
Extension members
C# 14 introduces extension members, one of the most exciting updates for anyone who's been using extension methods for years.
Before: Separate extension methods
Traditionally, if you wanted to add custom methods to an existing type, you would write multiple static methods in an extension class:
public static class ExtensionMembers
{
public static int BEFORE_CountValuesStartWithLetter(this IEnumerable<string> source, string letter)
=> source.Where(s => s.StartsWith(letter, StringComparison.OrdinalIgnoreCase)).Count();
public static int BEFORE_CountValuesEndWithLetter(this IEnumerable<string> source, string letter)
=> source.Where(s => s.EndsWith(letter, StringComparison.OrdinalIgnoreCase)).Count();
}
C# 14: Extension members inside an extension block
With C# 14, you can group extension members inside an extension block, making related methods easier to organise and maintain:
public static class ExtensionMembers
{
extension(IEnumerable<string> source)
{
public int CountValuesStartWithLetter(string letter)
=> source.Where(s => s.StartsWith(letter, StringComparison.OrdinalIgnoreCase)).Count();
public int CountValuesEndWithLetter(string letter)
=> source.Where(s => s.EndsWith(letter, StringComparison.OrdinalIgnoreCase)).Count();
}
}
Cleaner, more readable, and more maintainable.
The field keyword
The new field keyword in C# 14 simplifies how you handle property backing fields.
Before: Explicit private field
Originally if you wanted to validate the value being set in a property, you would have to set that value in a private field. Then you would get that private field when using the property's getter.
public class BEFORE_Product
{
// Private readonly field to store the Price property value
private decimal _price;
public decimal Price
{
// Gets the value from _price
get => _price;
// Sets the value to _price if 50 or over.
set => _price = value >= 50 ? value :
throw new ArgumentOutOfRangeException("Price must be at least 50");
}
}
C# 14: Use field in the setter
Now you can use the field keyword in the setter and the value set to that will be used in the getter.
public class Product
{
public decimal Price
{
get;
set => field = value >= 50 ? value :
throw new ArgumentOutOfRangeException("Price must be at least 50");
}
}
This removes the need for a manually defined private variable, while keeping validation logic clean and concise.
Unbound generic types in nameof
You can now use unbound generic types with the nameof operator.
Before: Specify the generic type when using nameof
public static string BEFORE_GetTypeName()
{
// Have to specify the int generic type. Then it would return "List"
return nameof(List<int>);
}
C# 14: No need to specify the generic type when using nameof
public static string GetTypeName()
{
// Would return "List" without specifying the generic type.
return nameof(List<>);
}
This is particularly useful for reflection and generic helper methods, where you don't need to specify a type argument.
Simple lambda parameters with modifiers
C# 14 simplifies lambda expressions by not having to specify the type for parameters and parameter modifiers like out, ref, and in.
Before: Specify parameter types from the delegate in the method
In this example, we have created a delegate called TryParseWord which has a number of parameters added to it.
When returning a delegate type in a method, you had to match the parameter types with what was in the delegate:
public static class SimpleLambdaParametersWithModifiers
{
public delegate bool TryParseWord<T>(string word, string letter, out T result);
public static TryParseWord<string> OldWay_TryParseWordWithStartingLetter =
// Types are specified in the parameters and must match what is in the TryParseWord delegate.
(string word, string letter, out string result) =>
{
if (!word.StartsWith(letter, StringComparison.OrdinalIgnoreCase))
{
result = string.Empty;
return false;
}
result = word;
return true;
};
}
C# 14: No need to specify parameter types from the delegate in the method
It will look at the parameters from the delegate and know what each parameter type is:
public static class SimpleLambdaParametersWithModifiers
{
public delegate bool TryParseWord<T>(string word, string letter, out T result);
public static TryParseWord<string> TryParseWordWithStartingLetter =
// No need to specify the types for the parameters.
(word, letter, out result) =>
{
if (!word.StartsWith(letter, StringComparison.OrdinalIgnoreCase))
{
result = string.Empty;
return false;
}
result = word;
return true;
};
}
Partial constructors and events
You can now declare partial constructors and partial events across multiple partial class files.
The way it works is that you would add the signature to the partial constructors and events in one partial class. For example, this could happen in an a file where the code is auto-generated:
// PartialConstructorsAndEvents.AutoGenerated.cs
public partial class PartialConstructorsAndEvents
{
private EventHandler _eventHandlers;
public string Name { get; }
// Partial constructor signature
public partial PartialConstructorsAndEvents(string name);
// Partial event signature
public partial event EventHandler? OnNameChange;
}
Then in another partial class, you would add the implementation:
// PartialConstructorsAndEvents.cs
public partial class PartialConstructorsAndEvents
{
// Partial constructor implementation
public partial PartialConstructorsAndEvents(string name)
{
Name = name;
}
// Partial event implementation
public partial event EventHandler? OnNameChange
{
add => _eventHandlers += value;
remove => _eventHandlers -= value;
}
}
User-defined compound assignment
When you use the += operator, it adds two numbers together and stores the value in the left-hand side variable.
For example:
var a = 2;
var b = 3;
a += b;
// Translates to a = a + b, so a would be 5.
You can now define your own compound assignment for the += operator in a class:
public class UserDefinedCompoundAssignment
{
public UserDefinedCompoundAssignment(decimal price)
{
Price = price;
SubTotal = price;
}
public decimal Price { get; private set; }
public decimal SubTotal { get; private set; }
// Override += operator
public void operator +=(UserDefinedCompoundAssignment addition)
{
// Subtotal is incremented with the Price property from the right-hand side.
SubTotal += addition.Price;
}
}
In the example below, b.SubTotal would equal to 66:
var a = new UserDefinedCompoundAssignment(44);
var b = new UserDefinedCompoundAssignment(22);
b += a;
// b.SubTotal = 66
Null-conditional assignment
You can now use the null-conditional operator on the left-hand side.
Before: Null-reference check
You have to do an if statement to see if the instance is null.
NullConditionAssignment? customer = null;
// Null-reference check on customer
if (customer != null)
{
customer.Name = "Steven";
}
C# 14: Use the null-conditional operator
You can now use the null-conditional operator ? when you know that an instance might be null:
NullConditionAssignment? customer = null;
customer?.Name = "Steven";
This line will simply do nothing if customer is null.
See these features in action
Like the features you are seeing? Watch the video below to see them in action:
Using C# 14 without .NET 10
Think you need to upgrade your whole project to .NET 10 to use C# 14? Think again.
You can use the latest C# features even in older .NET projects. This is as long as you have the .NET 10 SDK installed.
Simply open your .csproj file and set the <LangVersion> attribute to 14.0. For example:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>14.0</LangVersion> <!-- Set LangVersion here -->
</PropertyGroup>
</Project>
You can also set the <LangVersion> attribute to one of these values:
-
latest– Latest released version (including minor versions) -
latestMajorordefault– Latest major release version
So… is it worth updating?
If your codebase can benefit from cleaner syntax, better organisation, and new language features like field, extension, and partial constructors, then yes, C# 14 is worth adopting.
You don't necessarily need to update your project to .NET 10 right away, but having access to these features can streamline your code and boost productivity.
Want to try the code for yourself?
You can download the code example which includes each of the new features covered in this tutorial.
Related pages