Top 10 C# 13/14 and .NET 9/10 Features

Top 10 C# 13/14 and .NET 9/10 Features

Top 10 C# 13/14 and .NET 9/10 Features

If you are still on .NET 8, even though it is an LTS version, it will no longer be supported after November 2026. Since November 2024, two versions (.NET 9 and .NET 10) and two versions of C# have been released. Here are the interesting features introduced in .NET 9 and .NET 10 that could be part of your upgrade to .NET 10.

Table of contents


1. The field keyword

C# 13 (preview) / C# 14

The field keyword eliminates the need to declare a private backing field for a property with setter logic.

// Before
private string _name;
public string Name
{
    get => _name;
    set => _name = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
}

// After
public string Name
{
    get;
    set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
}

2. Null-conditional assignment

C# 14

The ?. operator now works on the left side of an assignment, not just for reading.

// Before
if (order != null)
    order.Client = client;

// After
order?.Client = client;

3. params collections

C# 13

params now accepts any collection type, including ReadOnlySpan<T>, which avoids allocating an array.

// Before: allocation guaranteed
void Display(params string[] items) { ... }

// After: zero allocation
void Display(params ReadOnlySpan<string> items)
{
    foreach (var item in items)
        Console.WriteLine(item);
}

Display("one", "two", "three");

4. System.Threading.Lock

C# 13 / .NET 9

There is now a dedicated type for locks instead of using an object.

// Before
private readonly object _lock = new();

lock (_lock)
{
    // critical section
}

// After
private readonly Lock _lock = new();

using (_lock.EnterScope())
{
    // critical section
}

EnterScope with using releases the lock even if an exception is thrown.


5. Task.WhenEach

.NET 9

Task.WhenEach lets you iterate over tasks as they complete, without managing the list manually.

// Before
var tasks = new List<Task<string>>(urls.Select(DownloadAsync));
while (tasks.Any())
{
    var completed = await Task.WhenAny(tasks);
    tasks.Remove(completed);
    Console.WriteLine(await completed);
}

// After
await foreach (var task in Task.WhenEach(urls.Select(DownloadAsync)))
{
    Console.WriteLine(await task);
}

6. Extension members

C# 14

Extension members now allow you to add properties and static members in addition to methods, inside an extension block.

public static class StringExtensions
{
    extension(string s)
    {
        public int WordCount =>
            s.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;

        public static string Empty => string.Empty;
    }
}

var count = "Hello world from C#".WordCount; // 4
var empty = string.Empty;

7. dotnet run app.cs

.NET 10

You can now run a C# file directly without creating a project, with directives for NuGet dependencies.

#!/usr/bin/env dotnet
#:package Humanizer@2.14.1

using Humanizer;

var number = 42;
Console.WriteLine(number.ToWords()); // forty-two
dotnet run app.cs

When the script grows, dotnet project convert turns it into a full project.


8. Built-in validation in Minimal APIs

.NET 10

Parameter validation is now built into Minimal APIs, without a custom filter or third-party library.

builder.Services.AddValidation();

app.MapPost("/products", (Product product) => Results.Ok(product));

public record Product(
    [Required] string Name,
    [Range(0.01, 10000)] decimal Price
);

If Name is empty or Price is out of range, the response is automatically a ProblemDetails with a 400 status.


9. Named query filters in EF Core

EF Core 10

Query filters now have a name, which lets you disable them individually with IgnoreQueryFilters instead of all at once.

modelBuilder.Entity<Article>()
    .HasQueryFilter("active", a => a.IsActive)
    .HasQueryFilter("tenant", a => a.TenantId == _tenantId);

var articles = await ctx.Articles
    .IgnoreQueryFilters("tenant")
    .ToListAsync();

10. HybridCache

ASP.NET Core 9

HybridCache combines IMemoryCache and IDistributedCache with a single API. It also handles cache stampede: if multiple requests arrive at the same time for a key not yet in cache, only one will fetch the data.

builder.Services.AddHybridCache();

var product = await _cache.GetOrCreateAsync(
    $"product-{id}",
    async cancel => await _repo.GetAsync(id, cancel)
);

Good luck with your upgrade to .NET 10.


This post was written with AI assistance and edited by me.


See also