Dajbych.net


Co je nového v .NET 9?

, 8 minut čtení

net2015 logo

.NET 9 přináší celou řada výkonnostních vylepšení a drobných změn. Tato verze však toho nepřináší tolik, že by to nějak radikálně změnilo způsob, jakým píšeme kód. V tom se dostatečně realizovaly 3 předchozí verze. Dokonce i jedna podstatnější změna – implicit extension types, která měla potenciál výrazně změnit způsob, jakým píšeme kód, byla nakonec ze C# 13 vyřazena a dočkáme se jí tedy nejspíš až za rok.

Co je nového v Base Class Library?

QUIC a HTTP/3

Možná tušíte, že je protokol HTTP/3 postaven na protokolu QUIC, který je v .NET už od verze 5 jako interní implementace. V .NET 7 už byla implementace experimentální a mohli ji zkoušet vývojáři. To se však od verze 9 mění a HTTP/3 je zapnuté by default.

HttpClient však podporuje HTTP/3 pouze v případě, kdy systém podporuje MsQuic. ASP.NET podporuje HTTP/3 se serverem Kestrel. IIS podporuje HTTP/3 v systému Windows Server 2022, když je nakonfigurováno TLS 1.3 a protokol HTTP/3 je povolen v registru.

Až to budete zkoušet, myslete na to, že prohlížeče v HTTP/3 nepodporují self-signed certifikáty (což je přesně to, co vám nastaví Visual Studio, pokud HTTPS zapnete).

LINQ Index

for cyklus používám prakticky už jen v případech, kdy potřebuji během procházení kolekce znát index každé položky. Když se změní typ kolekce, musí se v cyklu měnit Count na Length (nebo obráceně). To se v .NET 9 mění:

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

Metoda CountBy umožňuje jednodušší zápis v jednoduchých případech, kde bychom jinak museli použít metodu GroupBy:

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

Ke stejnému účelu slouží i metoda AggregateBy, jen s tím rozdílem, že místo sčítání stejných prvků provádí něco jiného:

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

Je možné vytvářet Guid takovým způsobem, že vygenerované hodnoty budou mít sekvenční pořadí:

var id = Guid.CreateVersion7();

To oceníte zejména v případě, kdy takto vygenerované hodnoty vkládáte do databáze, která má nad nimi index.

Lock

Dosud se dal použít lock na jakýkoliv referenční (nikoliv hodnotový) typ a osvědčeným postupem je vytvořit k tomuto účelu samostatný object:

private readonly object sync = new();

void DoWork() {
    lock (sync) {
		...
	}
}

Teď k tomuto účelu slouží nový typ Lock:

private readonly Lock sync = new();

void DoWork() {
	lock (sync) {
		...
	}
}

Je výkonnější, bezpečnější a hlavně už všechny budoucí vylepšení v tomto mechanismu získáte bez změny kódu.


Nezapomeňte si také přečíst tradiční souhrn výkonnostních vylepšení.

Co je nového v ASP.NET Core 9?

MapStaticAssets

Soubory v adresáři wwwroot se internetu zpřístupňují skrze volání:

app.UseStaticFiles();

Tento způsob však neznamená, že jsou ze strany aplikace soubory servírovány optimálně. Ve skutečnosti je potřeba ještě dalšími mechanismy zajistit, aby správně fungovala cache, například podle data poslední změny souboru. Jenže tento mechanismus funguje pouze v případě, kdy k nasazení vaší aplikace používáte WebDeploy z jednoho počítače. Pokud používáte continuous integration, pak mají po kompilaci všechny soubory nastavené datum vytvoření na čas, kdy kompilace proběhla. Všechny soubory mají tedy novější datum než na severu, takže se tam nahrají úplně všechny (bez ohledu na to, jestli jsou ve skutečnosti změněné, nebo ne). Protokol HTTP na to myslí a podporuje ETag, což je libovolný hash daného souboru. Nabízí se tedy možnost, aby .NET nějak automaticky dopočítával hashe jednotlivých souborů. A to právě přesně to dělá .NET 9 v novém middlewaru:

app.MapStaticAssets();

V Blazoru se statické soubory linkují přes vlastnost Assets:

<link rel="stylesheet" href="@Assets["style.css"]" />

V ASP.NET Core MVC nebo Razor Pages se linkují přes stávající tagy script, image nebo link, takže není potřeba dělat žádné změny.

Hybrid cache

HybridCache je abstraktní třída v samostatném balíčku 📦, která nahrazuje dosavadní IMemoryCache a IDistributedCache. Poskytuje jednotné rozhraní pro cachování bez ohledu na to, jestli jsou data uložená in-process nebo out-of-process. Cache zajišťuje, že se pro daný klíč vytvoří jeho hodnota pouze jednou, i když v průběhu jejího (asynchronního) vytváření je o hodnotu se stejným klíčem žádáno vícekrát opakovaně (efekt známý pod názvem cache stampede nebo také dog-piling). Její použití je jednoduché:

builder.Services.AddHybridCache();

...

public class DoWork(HybridCache hybridCache) {
    public async Task GetDataAsync(string key) {
        return await hybridCache.GetOrCreateAsync(key, async entry => {
            ...
        });
    }
}

Binary formatter

Třída BinaryFormatter, která je v .NETu od verze 1.1, slouží k (de)serializaci objektů přímo do datového proudu. Tedy přesněji sloužila. Od .NET 9 už bude vyhazovat NotSupportedException. Důvody k tomuto kroku hezky popsal Barry Dorrans, principal security program manager. Třída způsobem, jakým funguje, způsobila několik bezpečnostních zranitelností v Microsoft Exchage, Azure DevOps i Microsoft Sharepoint. Už nejde opravit bez toho, aniž by to nemělo zásadní dopady na zpětnou kompatibilitu. Původně měl Microsoft v plánu tuto třídu odstranit už v roce 2016, kdy vydával .NET Core 1.0, který ji už neobsahoval. Narazil ale na odpor vývojářů. Jednak někdo vzal její kód z .NET Frameworku a vytvořil z něj samostatný balíček 📦 pro .NET Core, a navíc Microsoft dostával mnoho emailů o tom, že velcí zákazníci kvůli tomu, že v něm tato třída není, nemůžou migrovat své projekty na .NET Core. Proto ji Microsoft do .NET Core s vydáním verze 1.1 raději vrátil.

Nicméně co dělat v případech, kdy není zbytí?

<PropertyGroup>
  <TargetFramework>net9.0</TargetFramework>
  <EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
</PropertyGroup>

<ItemGroup>
  <PackageReference Include="System.Runtime.Serialization.Formatters" Version="9.0.0-*" />
</ItemGroup>

No a je to. Žádné drama se nekoná. Pokud ale svůj projekt kódem uvedeným výše skutečně obohatíte, zálohujte si svou databázi opravdu pečlivě. 🙏

Co je nového v C# 13?

Implicit index access

Pokud jste potřebovali vytvořit pole a nastavit jeho hodnoty pomocí range indexů, museli jste ho nejprve inicializovat. To už teď není nutné, takže číslování indexů od konce můžete použít i při inicializaci pole:

var littleEndian = new byte[4] {
    [^1] = 0x4A,
    [^2] = 0x3B,
    [^3] = 0x2C,
    [^4] = 0x1D,
}; // 0x4A3B2C1D

Params collections

Metoda končící parametrem typu params už nemusí být jen pole, ale libovolný typ, který:

  1. implementuje rozhraní IEnumerable<T>
  2. má prostý konstruktor
  3. má metodu Add(T item)
var sum = SumNumbers(1, 2, 3, 4, 5);

int SumNumbers(params List<int> numbers) {
    return numbers.Sum();
}

Co je nového v .NET Aspire 9?

Pokud chcete používat nový .NET Aspire 9, musíte nejprve aktualizovat Visual Studio na verzi 17.12 nebo novější a musíte také následujícím způsobem upravit váš projekt:

<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 má novou metodu WaitFor, která zajišťuje, že se daná součást nezačne používat před tím, než se spustí:

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

Jednotlivé součásti mohou být snáze rozpoznány jako spuštěné, pokud mají 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

Stávající chování hostovacího procesu bylo takové, že se ukončením procesu vypnuly i kontejnery, které proces nastartoval. Toto chování jde teď změnit, takže je už možné ponechat kontejnery spuštěné:

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();

Co je nového v ML.NET 4?

ML.NET 4.0 přináší novou verzi balíčku 📦 s tokenizery Tiktoken a CodeGen a tokenizerem pro modely Llama. Následující příklad ukazuje použití tokenizeru pro model GPT-4:

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

Ve Visual Studiu si aktuálně můžete vybrat mezi těmito testovacími frameworky:

Zatímco jsou první tři zmíněné frameworky založené na reflexi, u frameworku TUnit tomu tak není. Ten je totiž založen na source generators, takže si rozumí i s aplikací, která se kompiluje v režimu AOT.