.NET 9: The Features Worth Knowing About

.NET 9, released in November 2024, is a Standard-Term Support (STS) release — supported for 18 months rather than the 3 years of an LTS release. It's not a revolution, but it delivers a set of targeted improvements that are worth understanding even if you stay on .NET 8 for now.

LINQ enhancements

CountBy and AggregateBy

Two new LINQ methods eliminate common GroupBy + Select patterns:

Example.cs
// Before
var tagCounts = articles
    .SelectMany(a => a.Tags)
    .GroupBy(t => t)
    .Select(g => new { Tag = g.Key, Count = g.Count() });

// After (.NET 9)
var tagCounts = articles
    .SelectMany(a => a.Tags)
    .CountBy(t => t);

AggregateBy is similar but for custom aggregations:

Example.cs
var salesByRegion = orders.AggregateBy(
    keySelector: o => o.Region,
    seed: 0m,
    func: (total, order) => total + order.Amount);

Both methods return IEnumerable<KeyValuePair<TKey, TValue>> and are more memory-efficient than GroupBy because they don't materialise groups.

Index

Index() gives you the element index alongside each element, replacing the Select((item, index) => ...) pattern:

Example.cs
foreach (var (index, item) in collection.Index())
{
    Console.WriteLine($"{index}: {item}");
}

Serialization improvements

System.Text.Json in .NET 9 adds several features:

JsonSerializerOptions.Web — a pre-configured singleton with web defaults (camelCase, case-insensitive reading, reading numbers from strings):

Example.cs
var json = JsonSerializer.Serialize(data, JsonSerializerOptions.Web);

Indentation customisation — you can now control the indentation character and size:

Example.cs
var options = new JsonSerializerOptions
{
    WriteIndented = true,
    IndentCharacter = ' ',
    IndentSize = 2
};

Respect nullable annotations — the serializer can now be configured to enforce nullability:

Example.cs
var options = new JsonSerializerOptions
{
    RespectNullableAnnotations = true
};

This throws a JsonException if a non-nullable property receives a null value during deserialization.

Performance: spans and searching

SearchValues for multiple values

SearchValues<T> was introduced in .NET 8, but .NET 9 expands it with SearchValues<string> for multi-string searching:

Example.cs
private static readonly SearchValues<string> s_keywords =
    SearchValues.Create(["async", "await", "Task", "ValueTask"],
        StringComparison.OrdinalIgnoreCase);

bool containsKeyword = s_keywords.ContainsAny(sourceCode);

This is heavily optimised for repeated searches over the same set of values.

AlternateLookup for dictionaries

Dictionary<TKey, TValue> gained an AlternateLookup feature for looking up values with a different key type, avoiding allocations:

Example.cs
var lookup = dictionary.GetAlternateLookup<ReadOnlySpan<char>>();
if (lookup.TryGetValue(someSpan, out var value))
{
    // Found without allocating a string from the span
}

This is particularly useful in parsers and high-throughput text processing.

ASP.NET Core improvements

Built-in OpenAPI support

ASP.NET Core 9 includes built-in OpenAPI document generation without Swashbuckle:

Program.cs
builder.Services.AddOpenApi();

// ...

app.MapOpenApi(); // Serves the OpenAPI document at /openapi/v1.json

You can customise the document with transformers:

Example.cs
builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, ct) =>
    {
        document.Info.Title = "My API";
        document.Info.Version = "v1";
        return Task.CompletedTask;
    });
});

HybridCache

HybridCache provides a unified L1/L2 caching API that combines IMemoryCache and IDistributedCache:

Program.cs
builder.Services.AddHybridCache(options =>
{
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(5),
        LocalCacheExpiration = TimeSpan.FromMinutes(1)
    };
});
Example.cs
public class ProductService(HybridCache cache)
{
    public async Task<Product> GetProductAsync(int id)
    {
        return await cache.GetOrCreateAsync(
            $"product-{id}",
            async ct => await _repository.FindAsync(id, ct));
    }
}

It handles cache stampede protection, serialization, and L1/L2 coordination automatically.

Runtime improvements

Server GC dynamic adaptation — the garbage collector now dynamically adjusts memory limits based on container memory pressure, reducing OOM kills in containerised workloads.

Loop optimisations — the JIT compiler gained better loop cloning, unrolling, and strength reduction, which benefits array and span processing.

ARM64 vectorisation improvements — significant SIMD improvements for ARM-based hosting environments like AWS Graviton.

Should you upgrade?

.NET 9 is an STS release, so it won't receive patches after May 2026. If you're currently on .NET 8 LTS, you have two reasonable strategies:

  1. Stay on .NET 8 until .NET 10 (the next LTS) arrives in November 2025, then upgrade directly.
  2. Move to .NET 9 now to adopt new features, with a plan to upgrade to .NET 10 soon after its release.

For greenfield projects, .NET 9 is the obvious choice. For production systems where stability and long-term support matter, .NET 8 remains the safer bet until .NET 10 lands.