.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]}");
}
}
}
}
Auch an weiteren Stellen hält System.ReadOnlySpan<T>
Einzug in die Basisklassenbibliothek:
ReadOnlySpan<T>
bietet nun wieString
die 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.
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)