.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ý:
- implementuje rozhraní
IEnumerable<T>
- má prostý konstruktor
- 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:
MSTest
NUnit
xUnit
TUnit 🆕
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.