Dajbych.net


What’s new in .NET 9?

, 9 minutes to read

net2015 logo

.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:

  1. implements interface IEnumerable<T>
  2. has a simple constructor
  3. 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:

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.