Neuerungen in .NET 9

Erfahren Sie mehr über die neuen Features in .NET 9, und finden Sie Links zu weiterführender Dokumentation.

.NET 9, der Nachfolger von .NET 8, legt einen besonderen Schwerpunkt auf cloudnative Apps und Leistung. Es wird 18 Monate als Release mit Standard-Laufzeitunterstützung (Standard-Term Support, STS) unterstützt. Sie können .NET 9 hier herunterladen.

Neu bei .NET 9 ist, dass das Entwicklungsteam Updates für die .NET 9-Vorschau unter GitHub Discussions veröffentlicht. Das ist ein großartiger Ort, um Fragen zu stellen und Feedback zum Release zu geben.

Dieser Artikel wurde für .NET 9 Preview 2 aktualisiert. In den folgenden Abschnitten werden die Updates der .NET-Kernbibliotheken in .NET 9 beschrieben.

.NET-Runtime

Serialisierung

.NET 9 verfügt in System.Text.Json über neue Optionen zum Serialisieren von JSON und einen neuen Singleton, der die Serialisierung mithilfe von Webstandardeinstellungen erleichtert.

Einzugsoptionen

JsonSerializerOptions enthält neue Eigenschaften, mit denen Sie das Einzugszeichen und die Einzugsgröße von geschriebenem JSON anpassen können.

var options = new JsonSerializerOptions
{
    WriteIndented = true,
    IndentCharacter = '\t',
    IndentSize = 2,
};

string json = JsonSerializer.Serialize(
    new { Value = 1 },
    options
    );
Console.WriteLine(json);
//{
//                "Value": 1
//}

Standardweboptionen

Wenn Sie mit den Standardoptionen serialisieren möchten, die ASP.NET Core für Web-Apps verwendet, verwenden Sie das neue JsonSerializerOptions.Web-Singleton.

string webJson = JsonSerializer.Serialize(
    new { SomeValue = 42 },
    JsonSerializerOptions.Web // Defaults to camelCase naming policy.
    );
Console.WriteLine(webJson);
// {"someValue":42}

LINQ

Neue Methoden, CountBy und AggregateBy, wurden eingeführt. Diese Methoden ermöglichen es, den Zustand nach Schlüssel zu aggregieren, ohne Zwischengruppierungen über GroupBy zuordnen zu müssen.

CountBy ermöglicht es Ihnen, die Häufigkeit der einzelnen Schlüssel schnell zu berechnen. Im folgenden Beispiel wird das Wort gesucht, das am häufigsten in einer Textzeichenfolge vorkommt.

string sourceText = """
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    Sed non risus. Suspendisse lectus tortor, dignissim sit amet, 
    adipiscing nec, ultricies sed, dolor. Cras elementum ultrices amet diam.
""";

// Find the most frequent word in the text.
KeyValuePair<string, int> mostFrequentWord = sourceText
    .Split(new char[] { ' ', '.', ',' }, StringSplitOptions.RemoveEmptyEntries)
    .Select(word => word.ToLowerInvariant())
    .CountBy(word => word)
    .MaxBy(pair => pair.Value);

Console.WriteLine(mostFrequentWord.Key); // amet

AggregateBy ermöglicht es Ihnen, allgemeinere Workflows zu implementieren. Das folgende Beispiel zeigt, wie Sie Bewertungen berechnen können, die einem bestimmten Schlüssel zugeordnet sind.

(string id, int score)[] data =
    [
        ("0", 42),
        ("1", 5),
        ("2", 4),
        ("1", 10),
        ("0", 25),
    ];

var aggregatedData =
    data.AggregateBy(
        keySelector: entry => entry.id,
        seed: 0,
        (totalScore, curr) => totalScore + curr.score
        );

foreach (var item in aggregatedData)
{
    Console.WriteLine(item);
}
//(0, 67)
//(1, 15)
//(2, 4)

Index<TSource>(IEnumerable<TSource>) ermöglicht es Ihnen, den impliziten Index eines aufzählbaren Elements schnell zu extrahieren. Sie können jetzt Code wie den folgenden Codeschnipsel schreiben, um Elemente in einer Auflistung automatisch zu indizieren.

IEnumerable<string> lines2 = File.ReadAllLines("output.txt");
foreach ((int index, string line) in lines2.Index())
{
    Console.WriteLine($"Line number: {index + 1}, Line: {line}");
}

Sammlungen

Der Sammlungstyp PriorityQueue<TElement,TPriority> im Namespace System.Collections.Generic enthält eine neue Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>)-Methode, mit der Sie die Priorität eines Elements in der Warteschlange aktualisieren können.

PriorityQueue.Remove()-Methode

.NET 6 hat die PriorityQueue<TElement,TPriority>-Auflistung eingeführt, die eine einfache und schnelle Array-Heap-Implementierung bereitstellt. Ein Problem mit Array-Heaps im Allgemeinen besteht darin, dass sie keine Prioritätsupdates unterstützen, wodurch sie für die Verwendung in Algorithmen wie Variationen des Dijkstra-Algorithmus prohibitiv sind.

Obwohl es nicht möglich ist, effiziente $O(\log n)$-Prioritätsupdates in der vorhandenen Auflistung zu implementieren, ermöglicht die neue PriorityQueue<TElement,TPriority>.Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>)-Methode das Emulieren von Prioritätsupdates (wenn auch bei $O(n)$ Zeit):

public static void UpdatePriority<TElement, TPriority>(
    this PriorityQueue<TElement, TPriority> queue,
    TElement element,
    TPriority priority
    )
{
    // Scan the heap for entries matching the current element.
    queue.Remove(element, out _, out _);
    // Re-insert the entry with the new priority.
    queue.Enqueue(element, priority);
}

Diese Methode entsperrt Benutzer, die Graphalgorithmen in Kontexten implementieren möchten, in denen die asymptotische Leistung kein Blocker ist. (Solche Kontexte umfassen Bildung und Prototyperstellung.) Hier ist beispielsweise eine Toy-Implementierung des Dijkstra-Algorithmus, die die neue API verwendet.

Kryptografie

Für die Kryptografie fügt .NET 9 dem CryptographicOperations-Typ eine neue One-Shot-Hashmethode hinzu. Außerdem werden neue Klassen hinzugefügt, die den KMAC-Algorithmus verwenden.

CryptographicOperations.HashData()-Methode

.NET enthält mehrere statische „One-Shot“-Implementierungen von Hashfunktionen und verwandten Funktionen. Zu diesen APIs gehören SHA256.HashData und HMACSHA256.HashData. One-Shot-APIs sind vorzugsweise zu verwenden, da sie die bestmögliche Leistung bieten und Zuordnungen reduzieren oder beseitigen können.

Wenn ein Entwickler eine API bereitstellen möchte, die Hashing unterstützt, bei der der Aufrufer definiert, welcher Hashalgorithmus verwendet werden soll, erfolgt dies in der Regel durch Akzeptieren eines HashAlgorithmName-Arguments. Die Verwendung dieses Musters mit One-Shot-APIs erfordert jedoch die Umstellung aller möglichen HashAlgorithmName und dann die Verwendung der entsprechenden Methode. Um dieses Problem zu beheben, führt .NET 9 die CryptographicOperations.HashData-API ein. Mit dieser API können Sie einen Hash oder HMAC über eine Eingabe als One-Shot erstellen, bei dem der verwendete Algorithmus von einem HashAlgorithmName bestimmt wird.

static void HashAndProcessData(HashAlgorithmName hashAlgorithmName, byte[] data)
{
    byte[] hash = CryptographicOperations.HashData(hashAlgorithmName, data);
    ProcessHash(hash);
}

KMAC-Algorithmus

.NET 9 stellt den KMAC-Algorithmus bereit, wie durch NIST SP-800-185 angegeben. KMAC (KECCAK Message Authentication Code) ist eine pseudozufällige Funktion und schlüsselgebundene Hashfunktion basierend auf KECCAK.

Die folgenden neuen Klassen verwenden den KMAC-Algorithmus. Verwenden Sie Instanzen zum Sammeln von Daten, um einen MAC zu erzeugen, oder verwenden Sie die statische HashData-Methode für einen One-Shot über eine einzelne Eingabe.

KMAC ist unter Linux mit OpenSSL 3.0 oder höher und unter Windows 11 Build 26016 oder höher verfügbar. Mithilfe der statischen IsSupported-Eigenschaft können Sie ermitteln, ob die Plattform den gewünschten Algorithmus unterstützt.

if (Kmac128.IsSupported)
{
    byte[] key = GetKmacKey();
    byte[] input = GetInputToMac();
    byte[] mac = Kmac128.HashData(key, input, outputLength: 32);
}
else
{
    // Handle scenario where KMAC isn't available.
}

Spiegelung

In .NET Core-Versionen und .NET 5–8 wurde die Unterstützung für das Erstellen einer Assembly und das Ausgeben von Reflexionsmetadaten für dynamisch erstellte Typen auf einen ausführbaren AssemblyBuilder beschränkt. Der Mangel an Unterstützung für das Speichern einer Assembly war häufig ein Blocker für Kunden, die von .NET Framework zu .NET migrieren. .NET 9 fügt öffentliche APIs zum Speichern einer ausgegebenen Assembly zu AssemblyBuilder hinzu.

Die neue, persistierte AssemblyBuilder-Implementierung ist unabhängig von der Runtime und Plattform. Verwenden Sie die neue AssemblyBuilder.DefinePersistedAssembly-API, um eine persistierte AssemblyBuilder-Instanz zu erstellen. Die vorhandene AssemblyBuilder.DefineDynamicAssembly-API akzeptiert den Assemblynamen und optionale benutzerdefinierte Attribute. Um die neue API zu verwenden, übergeben Sie die Kernassembly, System.Private.CoreLib, die für das Verweisen auf Basisruntimetypen verwendet wird. Für AssemblyBuilderAccess gibt es keine Option. Und zurzeit unterstützt die persistierte AssemblyBuilder-Implementierung nur das Speichern, nicht die Ausführung. Nachdem Sie eine Instanz des persistierten AssemblyBuilder erstellt haben, bleiben die nachfolgenden Schritte zum Definieren eines Moduls, Typs, einer Methode oder einer Enumeration, des Schreibens von IL und alle anderen Verwendungen unverändert. Dies bedeutet, dass Sie den vorhandenen System.Reflection.Emit-Code unverändert zum Speichern der Assembly verwenden können. Der folgende Code enthält hierzu ein Beispiel.

public void CreateAndSaveAssembly(string assemblyPath)
{
    AssemblyBuilder ab = AssemblyBuilder.DefinePersistedAssembly(
        new AssemblyName("MyAssembly"),
        typeof(object).Assembly
        );
    TypeBuilder tb = ab.DefineDynamicModule("MyModule")
        .DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);

    MethodBuilder mb = tb.DefineMethod(
        "SumMethod",
        MethodAttributes.Public | MethodAttributes.Static,
        typeof(int), [typeof(int), typeof(int)]
        );
    ILGenerator il = mb.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Add);
    il.Emit(OpCodes.Ret);

    tb.CreateType();
    ab.Save(assemblyPath); // or could save to a Stream
}

public void UseAssembly(string assemblyPath)
{
    Assembly assembly = Assembly.LoadFrom(assemblyPath);
    Type type = assembly.GetType("MyType");
    MethodInfo method = type.GetMethod("SumMethod");
    Console.WriteLine(method.Invoke(null, [5, 10]));
}

Leistung

.NET 9 enthält Verbesserungen für den 64-Bit-JIT-Compiler, die die Leistung von Anwendungen verbessern sollen. Zu diesen Compilererweiterungen gehören:

Arm64-Vektorisierung ist ein weiteres neues Feature der Laufzeit.

Schleifenoptimierungen

Die Verbesserung der Codegenerierung für Schleifen ist eine Priorität für .NET 9, und der 64-Bit-Compiler bietet eine neue Optimierung namens Induktionsvariablenerwiterung (IV).

Eine IV ist eine Variable, deren Wert sich als enthaltende Schleife durchläuft. In der folgenden for-Schleife i ist eine IV: for (int i = 0; i < 10; i++). Wenn der Compiler analysieren kann, wie sich der Wert einer IV über die Iterationen der Schleife entwickelt, kann er leistungsfähigeren Code für verwandte Ausdrücke erzeugen.

Betrachten Sie das folgende Beispiel, das ein Array durchläuft:

static int Sum(int[] arr)
{
    int sum = 0;
    for (int i = 0; i < arr.Length; i++)
    {
        sum += arr[i];
    }

    return sum;
}

Die Indexvariable i ist 4 Byte groß. Auf der Assemblyebene werden 64-Bit-Register typischerweise verwendet, um Arrayindizes auf x64 zu halten, und in früheren .NET-Versionen generierte der Compiler Code, der i für den Arrayzugriff auf 8 Byte erweiterte, aber i an anderer Stelle weiterhin als 4-Byte-Ganzzahl behandelte. Die Erweiterung von i auf 8 Byte erfordert jedoch bei x64 eine zusätzliche Anweisung. Mit der IV-Erweiterung erweitert der 64-Bit-JIT-Compiler nun i in der gesamten Schleife auf 8 Byte und lässt die Nullerweiterung weg. Schleifen über Arrays sind sehr häufig, und die Vorteile dieser Befehlsentfernung summieren sich schnell.

Inliningverbesserungen für Native AOT

Eines der Ziele von .NET für den Inliner des 64-Bit-JIT-Compilers ist es, so viele Einschränkungen wie möglich zu beseitigen, die das Inlinen einer Methode verhindern. .NET 9 ermöglicht das Inlining von Zugriffen auf threadspezifische Statiken unter Windows x64, Linux x64 und Linux Arm64.

Bei static-Klassenmitgliedern existiert genau eine Instanz des Mitglieds in allen Instanzen der Klasse, die sich das Mitglied „teilen“. Wenn der Wert eines static-Mitglieds für jeden Thread eindeutig ist, kann es die Leistung verbessern, wenn dieser Wert threadspezifisch gemacht wird, da dann kein Gleichzeitigkeitsprimitiv mehr erforderlich ist, um sicher auf das static-Mitglied von seinem enthaltenen Thread zuzugreifen.

Bisher musste der 64-Bit-JIT-Compiler bei Zugriffen auf threadspezifische statische Daten in nativen AOT-kompilierten Programmen einen Aufruf an die Laufzeitumgebung absetzen, um die Basisadresse des threadspezifischen Speichers zu ermitteln. Jetzt kann der Compiler diese Aufrufe einbinden, was zu einer wesentlich geringeren Anzahl von Anweisungen für den Zugriff auf diese Daten führt.

PGO-Verbesserungen: Typüberprüfungen und Umwandlungen

NET 8 aktiviert standardmäßig die dynamische profilgesteuerte Optimierung (PGO). NET 9 erweitert die PGO-Implementierung des 64-Bit-JIT-Compilers, um mehr Codemuster zu profilieren. Wenn die stufenweise Kompilierung aktiviert ist, fügt der 64-Bit-JIT-Compiler bereits Instrumente in Ihr Programm ein, um ein Profil seines Verhaltens zu erstellen. Bei der Neukompilierung mit Optimierungen nutzt der Compiler das Profil, das er zur Laufzeit erstellt hat, um Entscheidungen zu treffen, die sich auf den aktuellen Lauf Ihres Programms beziehen. In .NET 9 verwendet der 64-Bit-JIT-Compiler PGO-Daten, um die Leistung von Typüberprüfungen zu verbessern.

Um den Typ eines Objekts zu bestimmen, ist ein Aufruf in der Laufzeitumgebung erforderlich, was mit einem Leistungsverlust verbunden ist. Wenn der Typ eines Objekts überprüft werden muss, gibt der 64-Bit-JIT-Compiler diesen Aufruf aus Gründen der Korrektheit aus (Compiler können in der Regel nicht alle Möglichkeiten ausschließen, auch wenn sie unwahrscheinlich erscheinen). Wenn die PGO-Daten jedoch darauf hindeuten, dass es sich bei einem Objekt wahrscheinlich um einen bestimmten Typ handelt, gibt der 64-Bit-JIT-Compiler jetzt einen schnellen Pfad aus, der diesen Typ kostengünstig überprüft, und greift nur bei Bedarf auf den langsamen Pfad des Aufrufs zur Laufzeit zurück.

Arm64-Vektorisierung in .NET-Bibliotheken

Eine neue EncodeToUtf8-Implementierung macht sich die Fähigkeit des 64-Bit-JIT-Compilers zunutze, Multiregisterlade-/Speicherbefehle auf Arm64 zu emittieren. Dieses Verhalten ermöglicht es Programmen, größere Datenmengen mit weniger Anweisungen zu verarbeiten. .NET-Anwendungen in verschiedenen Bereichen sollten auf Arm64-Hardware, die diese Funktionen unterstützt, eine Verbesserung des Durchsatzes erfahren. Einige Benchmarks schneiden ihre Ausführungszeit um mehr als die Hälfte ab.

.NET SDK

Komponententest

In diesem Abschnitt werden die Aktualisierungen für Unittests in .NET 9 beschrieben: parallele Ausführung von Tests und die Testausgabe der Terminalprotokollierung.

Mit dieser Option können Sie Tests parallel ausführen

In .NET 9 ist dotnet test vollständig in MSBuild integriert. Da MSBuild die parallele Erstellung unterstützt, können Sie Tests für dasselbe Projekt in verschiedenen Zielframeworks parallel ausführen. Standardmäßig begrenzt MSBuild die Anzahl der parallelen Prozesse auf die Anzahl der Prozessoren auf dem Computer. Mit dem Schalter -maxcpucount können Sie auch Ihre eigene Grenze festlegen. Wenn Sie die Parallelität ausschalten möchten, setzen Sie die Eigenschaft TestTfmsInParallel-MSBuild auf false.

Testanzeige für die Terminalprotokollierung

Testergebnisberichte für dotnet test werden jetzt direkt in der MSBuild-Terminalprotokollierung unterstützt. Sie erhalten eine umfassendere Testberichterstattung sowohl während der Durchführung von Tests (Anzeige des Namens des laufenden Tests) als auch nach Abschluss der Tests (bessere Darstellung von Testfehlern).

Weitere Informationen zur Terminalprotokollierung finden Sie im Artikel zu „dotnet build“ unter Optionen.

.NET-Tool roll-forward

.NET-Tools sind frameworkabhängige Anwendungen, die Sie global oder lokal installieren und dann mit dem .NET-SDK und installierten .NET-Laufzeiten ausführen können. Diese Tools, wie alle .NET-Apps auch, richten sich an eine bestimmte Hauptversion von .NET. Standardmäßig laufen Anwendungen nicht auf neueren Versionen von .NET. Toolautoren konnten sich für die Ausführung ihrer Tools unter neueren Versionen der .NET-Laufzeitumgebung entscheiden, indem sie die RollForward-MSBuild Eigenschaft einstellten. Dies ist jedoch nicht bei allen Tools der Fall.

Mit einer neuen Option für dotnet tool install können die Benutzer*innen entscheiden, wie .NET-Tools ausgeführt werden sollen. Wenn Sie ein Tool über dotnet tool install installieren oder über dotnet tool run <toolname> ausführen, können Sie ein neues Flag namens --allow-roll-forward angeben. Mit dieser Option wird das Tool im Rollforwardmodus Majorkonfiguriert. In diesem Modus kann das Tool auf einer neueren Hauptversion von .NET ausgeführt werden, wenn die passende .NET-Version nicht verfügbar ist. Diese Funktion hilft Early Adopters bei der Verwendung von .NET-Tools, ohne dass die Toolautoren ihren Code ändern müssen.

Weitere Informationen