.NET 9 brings a number of performance improvements and minor changes. However, this version doesn't bring much to the table that it radically changes the way we write code. In this, the 3 previous versions were sufficiently implemented. Even one more significant change – implicit extension types, which had the potential to significantly change the way we write code, was eventually removed from C# 13 and will probably not see it for a year.
What’s new in Base Class Library?
QUIC and HTTP/3
You may know that the HTTP/3 protocol is built on the QUIC protocol, which has been in .NET since version 5 as an internal implementation. In .NET 7, the implementation was already experimental and could be tested by developers. However, this has changed since version 9 and HTTP/3 is enabled by default.
However, HttpClient
only supports HTTP/3 if the system supports MsQuic. ASP.NET supports HTTP/3 with Kestrel Server. IIS supports HTTP/3 in Windows Server 2022 when TLS 1.3 is configured and HTTP/3 is enabled in the registry.
When you try this, keep in mind that browsers in HTTP/3 do not support self-signed certificates (which is exactly what Visual Studio will set up for you if you turn on HTTPS).
LINQ Index
for
I use the cycle practically only in cases where I need to know the index of each item while browsing the collection. When the type of collection changes, Count
must change to Length
(or vice versa) in the cycle. This changes in .NET 9:
class Car(string brand, Color color) : IEqutable<Car> {
public string Brand => brand;
public Color Color => color;
public bool Equals(Car? other) => brand.Equals(other?.Brand);
}
List<Car> cars = [
new("Jaguar", Color.DarkGreen),
new("Land Rover", Color.SandyBrown),
new("Toyota", Color.Silver)
];
foreach (var (i, car) in cars.Index()) {
Console.WriteLine($"{i}. prize: {car.Brand}");
}
// Output:
// 1. prize: Jaguar
// 2. prize: Land Rover
// 3. prize: Toyota
LINQ CountBy
The CountBy method allows for easier entry in simple cases where we would otherwise have to use the GroupBy method:
List<Car> cars = [
new("Jaguar", Color.DarkGreen),
new("Jaguar", Color.DarkBlue),
new("Toyota", Color.Silver),
new("Toyota", Color.Red)
];
foreach (var brands in cars.CountBy(c => c.Brand)) {
Console.WriteLine($"There are {brands.Value} cars by {brands.Key}.");
}
// Output:
// There are 2 cars by Jaguar.
// There are 2 cars by Toyota.
LINQ AggregateBy
The AggregateBy method serves the same purpose, with the only difference being that instead of adding the same elements, it does something else:
public record Ride(Car car, float distance);
List<Ride> rides = [
new(new("Jaguar", Color.DarkGreen), 20),
new(new("Jaguar", Color.DarkGreen), 10),
];
foreach (var mileage in rides.AggregateBy(r => r.car, x => 0, (x, y) => x + y.distance)) {
Console.WriteLine($"The {mileage.Key.Brand}'s total mileage is {mileage.Value} km.");
}
// Output:
// The Jaguar's total mileage is 30 km.
UUID Version 7
It is possible to create Guid
in such a way that the generated values will have a sequential order:
var id = Guid.CreateVersion7();
This is especially useful if you are entering the values generated in this way into a database that has an index above them.
Lock
Until now, lock
could be applied to any reference (not value) type, and the best practice is to create a separate object
for this purpose:
private readonly object sync = new();
void DoWork() {
lock (sync) {
...
}
}
Now a new type of Lock
is used for this purpose:
private readonly Lock sync = new();
void DoWork() {
lock (sync) {
...
}
}
It is more powerful, safer and, most importantly, you will get all future improvements in this mechanism without changing the code.
Also, be sure to read the traditional summary of performance improvements.
What’s new in ASP.NET Core 9?
MapStaticAssets
Files in the wwwroot
directory are made available to the Internet by calling:
app.UseStaticFiles();
However, this method does not mean that the application serves the files optimally. In fact, there are still other mechanisms to ensure that cache works properly, for example according to the date of the last modification of the file. However, this mechanism only works if you use WebDeploy to deploy your application from one computer. If you are using continuous integration, then after compilation all files have a creation date set to the time when the compilation took place. All files have a newer date than the one in the north, so they will all be uploaded there (regardless of whether they are actually changed or not). The HTTP protocol thinks about this and supports ETag, which is any hash of a given file. There is therefore a possibility for .NET to somehow automatically calculate the hashes of individual files. And that’s exactly what .NET 9 does in the new middleware:
app.MapStaticAssets();
In Blazor, static files are linked through the Assets
property:
<link rel="stylesheet" href="@Assets["style.css"]" />
In ASP.NET Core MVC or Razor Pages, they link through existing script
, image
, or link
tags, so no changes need to be made.
Hybrid cache
HybridCache
is an abstract class in a separate package 📦, which replaces the existing IMemoryCache
and IDistributedCache
. It provides a uniform interface for caching, regardless of whether the data is stored in in-process or out-of-process. The cache ensures that a value with the same key is created only once, even if a value with the same key is requested several times during its (asynchronous) creation (an effect known as cache stampede or dog-piling). It is simple to use:
builder.Services.AddHybridCache();
...
public class DoWork(HybridCache hybridCache) {
public async Task GetDataAsync(string key) {
return await hybridCache.GetOrCreateAsync(key, async entry => {
...
});
}
}
Binary formatter
The BinaryFormatter class, which is in the . NET since version 1.1, it is used to (de)serialize objects directly into a data stream. Or more precisely, it was served. From .NET 9, NotSupportedException
will be thrown out. The reasons for this step have been nicely described by Barry Dorrans, principal security program manager. The way the class works, it has caused several security vulnerabilities in Microsoft Exchange, Azure DevOps, and Microsoft Sharepoint. It can no longer be fixed without major impacts on backward compatibility. Originally, Microsoft planned to remove this class in 2016, when it released .NET Core 1.0, which no longer included it. However, it ran into resistance from developers. On the one hand, someone took its code from the .NET Framework and created a separate package 📦 for .NET Core, and on top of that, Microsoft received many emails about the fact that large customers could not migrate their projects to .NET Core due to the fact that this class was not included. That’s why Microsoft preferred to return it to .NET Core with the release of version 1.1.
However, what to do in cases where there is no other way?
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Runtime.Serialization.Formatters" Version="9.0.0-*" />
</ItemGroup>
Well, that’s it. There is no drama. But if you really enrich your project with the code mentioned above, back up your database really carefully. 🙏
What’s new in C# 13?
Implicit index access
If you needed to create an array and set its values using range indexes, you had to initialize it first. This is no longer necessary, so you can use end-to-end index numbering when initializing an array as well:
var littleEndian = new byte[4] {
[^1] = 0x4A,
[^2] = 0x3B,
[^3] = 0x2C,
[^4] = 0x1D,
}; // 0x4A3B2C1D
Params collections
A method ending with a parameter of type params
does not have to be just an array, but any type that:
- implements interface
IEnumerable<T>
- has a simple constructor
- has the
Add(T item)
method
var sum = SumNumbers(1, 2, 3, 4, 5);
int SumNumbers(params List<int> numbers) {
return numbers.Sum();
}
What’s new in .NET Aspire 9?
To use the new .NET Aspire 9, you must first update Visual Studio to version 17.12 or later, and you must also modify your project as follows:
<Project Sdk="Microsoft.NET.Sdk">
+ <Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />
<ItemGroup>
- <PackageReference Include="Aspire.Hosting.AppHost" Version="8.2.1" />
+ <PackageReference Include="Aspire.Hosting.AppHost" Version="9.0.0" />
</ItemGroup>
</Project>
WaitFor
DistributedApplication
Builder has a new method WaitFor
that ensures that a given component is not used before it is executed:
var builder = DistributedApplication.CreateBuilder(args);
var rabbit = builder.AddRabbitMQ("rabbit");
builder.AddProject<Projects.WebApplication1>("api")
.WithReference(rabbit)
.WaitFor(rabbit); // Don't start "api" until "rabbit" is ready...
builder.Build().Run();
WithHttpHealthCheck
Individual components can be more easily recognized as triggered if they have health check:
var builder = DistributedApplication.CreateBuilder(args);
var catalogApi = builder.AddContainer("catalog-api", "catalog-api")
.WithHttpEndpoint(targetPort: 8080)
.WithHttpHealthCheck("/health"); // Adds a health check to the catalog-api resource.
builder.AddProject<Projects.WebApplication1>("store")
.WithReference(catalogApi.GetEndpoint("http"))
.WaitFor(catalogApi);
builder.Build().Run();
WithLifetime
The current behavior of the hosting process was such that when the process was terminated, the containers that started the process were also shut down. This behavior can now be changed, so it’s now possible to keep containers running:
var builder = DistributedApplication.CreateBuilder(args);
var queue = builder.AddRabbitMQ("rabbit")
.WithLifetime(ContainerLifetime.Persistent); // This container won't be stopped until it is stopped manually.
builder.AddProject<Projects.WebApplication1>("api")
.WithReference(queue).WaitFor(queue);
builder.Build().Run();
What’s new in ML.NET 4?
ML.NET 4.0 brings a new version of the package 📦 with tokenizers Tiktoken and CodeGen and tokenizer for Llama models. The following example shows the use of tokenizer for the GPT-4 model:
using Microsoft.ML.Tokenizers;
var tokenizer = TiktokenTokenizer.CreateForModel("gpt-4");
var text = "Hello, World!";
// encode to IDs
var encodedIds = tokenizer.EncodeToIds(text);
Console.WriteLine($"encodedIds = [{string.Join(", ", encodedIds)}]");
// Output:
// encodedIds = [9906, 11, 4435, 0]
TUnit
In Visual Studio, you can currently choose between the following test frameworks:
MSTest
NUnit
xUnit
🆕 TUnit
While the first three frameworks mentioned are based on reflection, this is not the case with the TUnit framework. It is based on source generators, so it also understands an application that is compiled in AOT.