At .NET conf 2025, the new version of C# was unveiled. In this article, we’ll explore the new features and how they can help us in our daily work.
To help you out, I’ve also created a GitHub repository with some code examples, which you can find here:
Extension members
When we talk about extension members, we’re referring to the ability to extend a class with new functionality (classes or methods).
Properties in extensions
One of the features released and discussed in version 13 of the language is the ability to add properties (extension properties) in addition to methods (extension methods).
Let’s say we want to add a property to the IEnumerable interface. First, we create a static class (ideally in a dedicated library to maximize reusability) and then add the new property:
public static class MyEnumerableExtensions {
extension<TSource>(IEnumerable<TSource> source) {
//Extension property
public bool ThereAreNoElements => !source.Any();
}
}
IEnumerable<int> myList = new List<int> { 1, 2, 3 };
myList.ThereAreNoElements; // --> FalseType extensions
In the previous example, the ThereAreNoElements property was available on an instantiated class. However, it’s also possible to create extension methods and properties on types themselves. Building on the IEnumerable example, let’s add two elements: an example and a function to merge, plus an extension of the + operator.
public static class MyEnumerableExtensions {
extension<TSource>(IEnumerable<TSource>) {
// Static extension method
public static IEnumerable<TSource> MyCustomMerge(IEnumerable<TSource> first, IEnumerable<TSource> second){
//...
}
}
//Static extension of operator +
public static IEnumerable<TSource> operator +(IEnumerable<TSource> first, IEnumerable<TSource> second) {
//...
}
}
IEnumerable<int> myList1 = new List<int> { 1, 2, 3 };
IEnumerable<int> myList2 = new List<int> { 4, 5, 6 };
IEnumerable<int>.MyCustomMerge(myList1,myList2) // --> 1,2,3,4,5,6
myList1 + myList2 // --> 1,2,3,4,5,6The new field keyword
This new feature is really interesting, and I think I’ve wanted it many times. Remember when you had to create a public property and a private one to handle value setting? Well, I deliberately used the past tense because now it’s no longer necessary to write a private property, the new field keyword is resolved at the compiler level, allowing us to achieve the same result as before but with less code.
// Before C# 14 ☹️
private int _serialNumber;
public int SerialNumber
{
get => _serialNumber;
set => _serialNumber = value ?? throw new ArgumentNullException("Serial number is required");
}
//Now with C# 14 😍
public int SerialNumber
{
get;
set => field = value ?? throw new ArgumentNullException("Serial number is required");
}Null value assignments
Let’s say we have a class with a null value. What should I do to access a property of the object to assign it a value? Today, I necessarily have to verify that the object is instantiated before proceeding with the assignment (otherwise an exception would be generated).
In C# 14, assignment on null values will be handled directly by the compiler, and no exception will be generated (obviously, we’ll need to use the null-conditional operator).
// Before C# 14
if (myNewLightSaber is not null)
{
myNewLightSaber.mainCristal = new KyberCristal(123213, "Green");
}
// With C# 14
myNewLightSaber?.mainCristal = new KyberCristal(123213, "Green");Implicit Span<T> type conversion
In this article, I talked about Span and ReadOnlySpan, which we can define as “views” over data from other objects—a characteristic that makes them highly performant in certain contexts (if you want to learn more, read the dedicated article).
In C# 14, there will be an implicit conversion from our object to Span, as in the example below:
public void PrintArray(Span<int> list)
{
...
}
public void PrintArrayAgain(ReadonlySpan<int> list)
{
...
}
//Automatic cast from int[] to Span<int>/ReadonlySpan<int>
int[] myArray = { 1, 2, 3, 4, 5 };
PrintArray(myArray); // --> "1,2,3,4,5"
PrintArrayAgain(myArray); // --> "1,2,3,4,5"Anonymous arguments in lambda expressions
Before C# 14, it was mandatory to define the type in lambda expressions. Now the type is inferred based on the expression’s content: in case of violation, the code won’t compile.
delegate bool TryParse<T>(string text, out T result);
// Before C# 14
TryParse<int> parseOld = (string text, out int result) => Int32.TryParse(text, out result);
// With C#14
TryParse<int> parseNew = ( text, out result) => Int32.TryParse(text, out result);
int parsedValue = 0;
parseNew("2", out parsedValue); // It works!
parseNew( 2 , out parsedValue); // Doesn't compile!Nameof
The nameof expression can now receive an unbound generic type as input, for example List<>
// Before
nameof(List<int>) // --> List
nameof(Dictionary<string, int>) // --> Dictionary
//With C# 14
nameof(List<>) // --> List
nameof(Dictionary<,>) // --> DictionaryConclusioni
In this article, we’ve explored the latest features related to the new version of C#, presented at .NET conf 2025.
If you’re curious to discover more about .NET 10, I recommend reading the other dedicated articles on this blog!
See you next time 😉