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
fieldkeyword - 2. Null-conditional assignment
- 3.
paramscollections - 4.
System.Threading.Lock - 5.
Task.WhenEach - 6. Extension members
- 7.
dotnet run app.cs - 8. Built-in validation in Minimal APIs
- 9. Named query filters in EF Core
- 10.
HybridCache
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.