.NET 9.0 Preview 6 bringt einige lang ersehnte Funktionen

Seite 2: Weitere Neuerungen in der .NET-Basisklassenbibliothek in .NET 9.0 Preview 6

Inhaltsverzeichnis

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]}");
   }
  }
 }
}

Der Screenshot zeigt die Ausgabe des Beispielcodes (Abb. 1).

(Bild: Screenshot (Holger Schwichtenberg))

Auch an weiteren Stellen hält System.ReadOnlySpan<T> Einzug in die Basisklassenbibliothek:

  • ReadOnlySpan<T> bietet nun wie String die Methoden StartsWith() und EndWith().
  • In der IO.File-Klasse können Entwicklerinnen und Entwickler nun direkt mit WriteAllText() Zeichenketten in Form von ReadOnlySpan<char> persistieren.
  • Analog gibt es bei WriteAllBytes() eine neue Überladung für Bytefolgen, die als ReadOnlySpan<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);
  }
 }
}

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>

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.

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.

Der Code-Fixer macht Vorschläge, wenn man die OpenAPI-Unterstützung in einem per Native AOT kompilierten Web-API-Projekt installieren will (Abb. 2).

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

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>

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.

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)