C# 14

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:

C#
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; // --> False

Type 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.

C#
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,6

The 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.

C#
// 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).

C#
// 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:

C#
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.

C#
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<>

C#
// Before 
nameof(List<int>) // --> List
nameof(Dictionary<string, int>) // --> Dictionary

//With C# 14
nameof(List<>) // --> List
nameof(Dictionary<,>) // --> Dictionary

Conclusioni

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 😉

Share this article
Shareable URL
Prev Post

Claude 4.5

Next Post

ASP.NET Core 10 – What’s new!

Read next

Hybrid Cache released!

Hey devs! Great news – .NET 9.0.3 just dropped a few days ago, and with it comes the long-awaited official…
Hybrid cache released header image

.NET 9 Hybrid Cache

In this article, we’ll delve into one of Microsoft’s latest innovations, currently in preview at the time of…