.NET 9.0 Preview 6 bringt einige lang ersehnte Funktionen
Seite 2: Weitere Neuerungen in der .NET-Basisklassenbibliothek in .NET 9.0 Preview 6
Es gibt nun Konvertierungsmethoden zwischen den Vektor-Typen Vector2, Vector3, Vector4, Quaternion und Plane im Namensraum System.Numerics, wie folgende Beispiele zeigen:
Vector4 vector4 = new(1, 2, 3, 4);
Console.WriteLine(vector4);
Vector3 vector3 = vector4.AsVector3();
Console.WriteLine(vector3);
Die Klasse System.Numerics.BigInteger, die bisher eine beliebig lange Zahl enthalten durfte, hat Microsoft auf (2^31) – 1, also rund 2,14 Milliarden Bits begrenzt. Damit kann man immer noch Zahlen mit 646,5 Millionen Ziffern abspeichern, bei denen jede einzelne Instanz 256 MB benötigt. Microsoft begründet auf GitHub das Vorgehen.
Der Source-Generator für Protokollierung funktioniert nun auch mit den in C# 12.0 im Rahmen von .NET 8.0 eingeführten Primärkonstruktoren, beispielsweise
public partial class PersonLogger(ILogger logger)
{
[LoggerMessage(0, LogLevel.Information, "Login")]
public partial void Login();
}
FĂĽr die Handhabung von URLs, die im Standard Base64URL vorliegen, gibt es die neue Klasse System.Buffers.Text.Base64Url.
Im neuen Namensraum System.Net.ServerSentEvents gibt es die Klasse fĂĽr Server-Sent Events (SSE) zum Streamen vom Server zum Client als Alternative zu Websockets.
Die Klasse System.Text.RegularExpressions.Regex bietet eine neue Methode EnumerateSplits() als Ergänzung zu der bestehenden Methode Split(). Letztere spaltet eine Zeichenkette (Klasse System.String) in Teile auf, entsprechend der angegebenen Trennungszeichen. EnumerateSplits() erwartet hingegen als Eingabe die Zeichenkette als ReadOnlySpan<char> und liefert als Rückgabe Range-Objekte, die auf Teile der Eingabemenge zeigen. Damit werden jegliche Speicherallokationen vermieden.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace NET9_Console.FCL90
{
class FCL9_RegEx
{
public void Run()
{
CUI.Demo(nameof(FCL9_RegEx));
CUI.H2("Split() erwartet String, liefert Strings");
string URLs = "www.IT-Visions.de\nwww.dotnet9.de\nwww.dotnet-lexikon.de";
foreach (string s in Regex.Split(URLs, "[.\n]", RegexOptions.Multiline))
{
Console.WriteLine($"Teil: {s}");
}
CUI.H2("EnumerateSplits() erwartet ReadOnlySpan<char> liefert Range in Span<T>");
ReadOnlySpan<char> URlsAsSpan = URLs;
foreach (Range r in Regex.EnumerateSplits(URlsAsSpan, "[.\n]", RegexOptions.Multiline))
{
Console.WriteLine($"Teil: {URlsAsSpan[r]}");
}
}
}
}
(Bild:Â Screenshot (Holger Schwichtenberg))
Auch an weiteren Stellen hält System.ReadOnlySpan<T> Einzug in die Basisklassenbibliothek:
ReadOnlySpan<T>bietet nun wieStringdie MethodenStartsWith()undEndWith().- In der
IO.File-Klasse können Entwicklerinnen und Entwickler nun direkt mitWriteAllText()Zeichenketten in Form vonReadOnlySpan<char>persistieren. - Analog gibt es bei
WriteAllBytes()eine neue Ăśberladung fĂĽr Bytefolgen, die alsReadOnlySpan<byte>vorliegen.
Folgender Code zeigt die neuen Einsatzgebiete fĂĽr System.ReadOnlySpan<T>:
namespace NET9_Console.FCL90;
internal class FCL9_Spans
{
public void Run()
{
CUI.Demo(nameof(FCL9_Spans));
string path1 = @"c:\temp\info.txt";
string path2 = @"c:\temp\info.bin";
ReadOnlySpan<char> text = ".NET 9.0 erscheint als Nachfolger von .NET 8.0 im November 2024!";
if (text.StartsWith(".NET") && text.EndsWith("!"))
{
File.WriteAllText(path1, text);
CUI.Success("Gespeichert in " + path1);
ReadOnlySpan<byte> bytes = Encoding.UTF8.GetBytes(text.ToString());
File.WriteAllBytes(path2, bytes);
CUI.Success("Gespeichert in " + path2);
}
}
}
Neuerungen fĂĽr die JSON-Serialisierung
Der JSON-Serialisierer System.Text.Json besitzt nun eine neue Klasse System.Text.Json.Schema.JsonSchemaExporter, die die Metadaten zu einem .NET-Typ in JSON-Form liefert, in der Form wie diese auch bei der OpenAPI Specification einer Web-API zum Einsatz kommen. Die folgenden Codezeilen liefern die Metadaten fĂĽr die in den ersten beiden Codebeispielen des Artikels implementierte Klasse PersonWithAutoID:
{
"type": [
"object",
"null"
],
"properties": {
"ID": {
"type": "integer"
},
"Name": {
"type": [
"string",
"null"
]
}
}
}
Wenn man die Klasse PersonWithAutoID mit der Annotation [JsonDerivedType(typeof(PersonWithAutoID), typeDiscriminator: "Person")] versieht, erhält man den Typ-Diskriminator unter $type in den Metadaten:
{
"type": [
"object",
"null"
],
"required": [
"$type"
],
"anyOf": [
{
"properties": {
"$type": {
"const": "Person"
},
"ID": {
"type": "integer"
},
"Name": {
"type": [
"string",
"null"
]
}
}
}
]
}
Anpassungen für die Schemagenerierung sind möglich. So sorgt der folgende Programmcode
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
IndentSize = 5,
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
var schemaOptions = new JsonSchemaExporterOptions
{
TransformSchemaNode = (context, node) =>
{
Console.WriteLine(context.TypeInfo);
if (context.TypeInfo.Type == typeof(int))
{
node["min"] = "1";
node["max"] = "100000000";
}
return node;
}
};
JsonNode schema2 =
JsonSchemaExporter.GetJsonSchemaAsNode(options,
typeof(PersonWithAutoID),
schemaOptions);
Console.WriteLine(schema2);
dafĂĽr, dass bei allen Integer-Zahlen ein Wertebereich via min und max angegeben wird:
"id": {
"type": "integer",
"min": "1",
"max": "100000000"
Beim Serialisieren und Deserialisieren kann System.Text.Json nun auch berĂĽcksichtigen, ob eine Property null verbietet. Ist das der Fall und steht die neue Serialisierungsoption RespectNullableAnnotations auf true, erzeugt die Serialisierung beziehungsweise Deserialisierung einen Laufzeitfehler. Im folgenden Listing scheitern die Versuche, das Entwickler-Objekt zu serialisieren beziehungsweise zu deserialisieren, weil durch den zu Beginn der Datei aktivierten Nullable-Kontext die Eigenschaft Name in der Record-Klasse Entwickler keinen null-Wert erlaubt:
#nullable enable
namespace NET9_Console.FCL90;
internal class FCL9_JSON
{
record Entwickler(int ID, string Name);
public void JSON_NullableAnnotations()
{
JsonSerializerOptions options = new() { RespectNullableAnnotations = true };
CUI.H1("Serialisieren von einem Objekt mit Non-Nullable Property");
try
{
var json = JsonSerializer.Serialize(new Entwickler(42, null), options);
Console.WriteLine(json);
}
catch (Exception ex)
{
CUI.Error(ex);
}
CUI.H1("Deserialisieren von einem Objekt mit Non-Nullable Property");
try
{
var json = """
{ "ID": 42,
"Name" : null
}
""";
JsonSerializer.Deserialize<Entwickler>(json, options);
}
catch (Exception ex)
{
CUI.Error(ex);
}
}
}
Wenn statt #nullable enable die Annotation [DisallowNull] einsetzt,
record Entwickler(int ID, [DisallowNull] string Name);
kommt es nur zum Laufzeitfehler bei der Deserialisierung; die Serialisierung funktioniert jedoch.
Man kann den JSON-Serialisierer mit einer Einstellung in der Projektdatei global dazu bewegen, diese neue PrĂĽfung durchzufĂĽhren:
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Text.Json.JsonSerializerOptions.RespectNullableAnnotations" Value="true" />
</ItemGroup>
Hash-Werte fĂĽr statische Dateien
In ASP.NET Core hängt Microsoft nun bei allen aus der Sicht des Servers statischen Dateien (CSS-Dateien, JavaScript-Dateien, Grafiken und Videos) automatisch an den Dateinamen einen Hashwert für den Inhalt an, damit nicht veraltete Ressourcen aus dem Browsercache geladen werden.
In den Projektvorlagen fĂĽr ASP.NET Model-View-Controller, ASP.NET Razor Pages und Blazor Web App hat Microsoft dafĂĽr den bisherigen Aufruf in der Startdatei Program.cs app.UseStaticFiles(); ersetzt durch app.MapStaticAssets();.
Während in ASP.NET Model-View-Controller und ASP.NET Razor Pages die integrierten Tag Helper <link>, <script> und <image> automatisch den Hash an alle Pfade anhängen, muss man in Blazor eine spezielle Syntax mit der Seitendirektive @Assets nutzen:
<link rel="stylesheet" href="@Assets["bootstrap/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["Anwendungsname.styles.css"]" />
Aus den Beispielzeilen wird zur Laufzeit
<link rel="stylesheet" href="bootstrap/bootstrap.min.bpk8xqwxhs.css" />
<link rel="stylesheet" href="app.da95v2qkru.css" />
<link rel="stylesheet" href="Anwendungsname.wthn6682r1.styles.css" />
Damit auch die Import Maps fĂĽr JavaScript-Dateien funktionieren, muss man in Blazor in der Anwendungsrahmen-HTML-Datei App.razor das Tag <ImportMap /> setzen.
In ASP.NET Model-View-Controller und ASP.NET Razor Pages muss an der entsprechenden Stelle (_Layout.cshtml) <script type="importmap"></script> stehen.
Fortschritte bei Microsoft.AspNetCore.OpenApi
FĂĽr die neue Microsoft-Implementierung des OpenAPI-Standards fĂĽr Metadaten in Web-APIs in Form des Pakets Microsoft.AspNetCore.OpenApi aus der .NET 9.0 Preview 4 gibt es nun einen Code-Fixer in Visual Studio, der bei der Eingabe von AddOpenApi() und MapOpenApi() nahelegt, das notwendige NuGet-Paket "Microsoft.AspNetCore.OpenApi" zu installieren und den passenden Namensraum einzubinden.
(Bild:Â Screenshot (Holger Schwichtenberg))
Bei AddOpenApi() können Entwicklerinnen und Entwickler einen Schema-Transformer angegeben, der Veränderungen am Schema-Dokument vornimmt (siehe auch oben gezeigtes Beispiel mit JsonSchemaExporterOptions):
<button class="btn @(this.RendererInfo.IsInteractive ? "btn-primary" : "btn-danger")" @onclick="()=>IncrementCount(1)" disabled="@(!this.RendererInfo.IsInteractive)">+1</button>
Die OpenAPI-Implementierung in NuGet-Paket "Microsoft.AspNetCore.OpenApi" ist nun Standard in der Projektvorlage "ASP.NET Core Web API" anstelle des Community-Pakets "Swashbuckle.AspNetCore". "Microsoft.AspNetCore.OpenApi" wird noch nicht in die Projektvorlage "ASP.NET Core Web API (native AOT)" eingebunden, funktioniert in Tests, aber seit Preview 5 auch zusammen mit dem Native-AOT-Compiler. "Swashbuckle.AspNetCore" beherrscht seit Version 6.6 vom 18. Mai 2024 auch die Native-AOT-Kompilierung. Vorteil von "Swashbuckle.AspNetCore" gegenüber "Microsoft.AspNetCore.OpenApi" ist, dass "Swashbuckle.AspNetCore" nicht nur eine JSON-Darstellung der Metadaten liefert, sondern auch HTML- und JavaScript-basierte Hilfeseiten mit Möglichkeiten für Testaufrufe der Web-APIs bietet.
Weitere Neuerungen fĂĽr ASP.NET Core
In ASP.NET-Core-Web-APIs warnt ein neuer Analyzer, wenn man versucht, mit [Authorize] einen in einer Basisklasse festgelegten Zugang für jedermann [AllowAnonymous] einzuschränken, was nicht möglich ist. In dem Fall erscheint die Warnung "ASP0026 [Authorize] overridden by [AllowAnonymous] from farther away".
Wie schon im Zusammenhang mit .NET 9.0 Preview 5 angekündigt, hat Microsoft die Eigenschaft Platform zum Ermitteln des aktuellen Render-Modus in Blazor in RendererInfo umbenannt. Eine Schaltfläche, die deaktiviert ist und eine andere Farbe hat, wenn der aktuelle Render-Modus keine Interaktivität im Client erlaubt, sieht nun folgendermaßen aus:
<button class="btn @(this.RendererInfo.IsInteractive ? "btn-primary" : "btn-danger")"
@onclick="()=>IncrementCount(1)" disabled="@(!this.RendererInfo.IsInteractive)">+1
</button>
Nichts Neues bei MAUI und Entity Framework Core
Im Cross-Plattform-Framework .NET Multi-Platform App UI (MAUI) gibt es keine funktionalen Neuerungen in Preview 6, sondern das Entwicklungsteam hat sich darauf konzentriert, Fehler zu beheben. Auch in Entity Framework Core scheint es dieses Mal keine Neuerungen zu geben, denn in den Release Notes zu .NET 9.0 Preview 6 wird der objektrelationale Mapper nicht erwähnt.
Ausblick
Im August ist noch eine letzte Preview-Version von .NET 9.0 geplant. Im September und Oktober wird es dann als Release Candidate bezeichnete Versionen geben, bevor im November das stabile Release erscheint, das iX und der dpunkt.verlag zusammen mit www.IT-Visions.de bei der Online-Konferenz betterCode() .NET 9.0 am 19. November 2024 im Detail vorstellen werden.
(rme)