.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:
// 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:
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:
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):
var json = JsonSerializer.Serialize(data, JsonSerializerOptions.Web);
Indentation customisation — you can now control the indentation character and size:
var options = new JsonSerializerOptions
{
WriteIndented = true,
IndentCharacter = ' ',
IndentSize = 2
};
Respect nullable annotations — the serializer can now be configured to enforce nullability:
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:
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:
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:
builder.Services.AddOpenApi();
// ...
app.MapOpenApi(); // Serves the OpenAPI document at /openapi/v1.json
You can customise the document with transformers:
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:
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5),
LocalCacheExpiration = TimeSpan.FromMinutes(1)
};
});
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:
- Stay on .NET 8 until .NET 10 (the next LTS) arrives in November 2025, then upgrade directly.
- 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.