Nouveautés de .NET 9

Découvrez les nouvelles fonctionnalités de .NET 9 et des liens vers d’autres documentations.

.NET 9, le successeur de .NET 8, met l’accent sur les applications sur les applications natives Cloud et les performances. Il sera pris en charge pendant 18 mois en tant que version avec prise en charge à terme standard (STS). Vous pouvez télécharger .NET 9 ici.

Une nouveauté de .NET 9 : l’équipe d’ingénierie publie les mises à jour en préversion de .NET 9 sur GitHub Discussions. C’est un endroit idéal pour poser des questions et fournir des commentaires sur la version.

Cet article a été mis à jour pour .NET 9 Preview 2. Les sections suivantes décrivent les mises à jour apportées aux bibliothèques .NET principales dans .NET 9.

Runtime .NET

Sérialisation

Dans System.Text.Json, .NET 9 a de nouvelles options pour sérialiser le JSON et un nouveau singleton qui facilite la sérialisation en utilisant des valeurs par défaut pour le web.

Options d’indentation

JsonSerializerOptions inclut de nouvelles propriétés qui vous permettent de personnaliser le caractère de mise en retrait et la taille de la mise en retrait du JSON écrit.

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

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

Options web par défaut

Si vous voulez sérialiser avec les options par défaut utilisées par ASP.NET Core pour les applications web, utilisez le nouveau singleton JsonSerializerOptions.Web.

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

LINQ

De nouvelles méthodes CountBy et AggregateBy ont été introduites. Ces méthodes permettent d’agréger l’état par clé sans avoir besoin d’allouer des regroupements intermédiaires via GroupBy.

CountBy vous permet de calculer rapidement la fréquence de chaque clé. L’exemple suivant recherche le mot qui est présent le plus fréquemment dans une chaîne de texte.

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 vous permet d’implémenter des workflows à usage général. L’exemple suivant montre comment calculer les scores associés à une clé donnée.

(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>) permet d’extraire rapidement l’index implicite d’un énumérable. Vous pouvez maintenant écrire du code comme l’extrait de code suivant pour indexer automatiquement les éléments d’une collection.

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

Collections

Le type de collection PriorityQueue<TElement,TPriority> dans l’espace de noms System.Collections.Generic inclut une nouvelle méthode Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>) que vous pouvez utiliser pour mettre à jour la priorité d’un élément dans la file d’attente.

Méthode PriorityQueue.Remove()

.NET 6 a introduit la collection PriorityQueue<TElement,TPriority>, qui fournit une implémentation simple et rapide des tas sous forme de tableaux. Un des problèmes généraux liés aux tas sous forme de tableaux est qu’ils ne prennent pas en charge les mises à jour des priorités, ce qui interdit leur utilisation dans des algorithmes tels que des variantes de l’algorithme de Dijkstra.

Bien qu’il ne soit pas possible d’implémenter des mises à jour des priorités $O(\log n)$ efficaces dans la collection existante, la nouvelle méthode PriorityQueue<TElement,TPriority>.Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>) permet d’émuler des mises à jour des priorités (quoique avec une durée $O(n)$) :

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

Cette méthode permet aux utilisateurs d’implémenter des algorithmes de graphe dans des contextes où les performances asymptotiques ne sont pas un facteur bloquant. (Ces contextes sont par exemple l’enseignement et le prototypage.) Par exemple, voici une implémentation simplifiée de l’algorithme de Dijkstra qui utilise la nouvelle API.

Chiffrement

Pour le chiffrement, .NET 9 ajoute une nouvelle méthode de hachage one-shot (ponctuel) sur le type CryptographicOperations. Il ajoute également de nouvelles classes qui utilisent l’algorithme KMAC.

Méthode CryptographicOperations.HashData()

.NET inclut plusieurs implémentations « one-shot » statiques des fonctions de hachage et des fonctions associées. Ces API incluent SHA256.HashData et HMACSHA256.HashData. Il est préférable d’utiliser des API one-shot, car elles peuvent fournir les meilleures performances possibles, et réduire ou éliminer les allocations.

Si un développeur veut fournir une API qui prend en charge un hachage où l’appelant définit l’algorithme de hachage à utiliser, cela se fait généralement en acceptant un argument HashAlgorithmName. Cependant, l’utilisation de ce modèle avec des API one-shot nécessite de passer à chaque HashAlgorithmName possible, puis d’utiliser la méthode appropriée. Pour résoudre ce problème, .NET 9 introduit l’API CryptographicOperations.HashData. Cette API vous permet de produire un hachage ou un code HMAC sur une entrée sous une forme ponctuelle, où l’algorithme utilisé est déterminé par un HashAlgorithmName.

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

Algorithme KMAC

.NET 9 fournit l’algorithme KMAC tel que spécifié par NIST SP-800-185. KMAC (KECCAK Message Authentication Code) est une fonction pseudo-aléatoire et une fonction de hachage de clé basée sur KECCAK.

Les nouvelles classes suivantes utilisent l’algorithme KMAC. Utilisez des instances pour accumuler des données afin de produire un MAC ou utilisez la méthode statique HashData pour un hachage one-shot (ponctuel) sur une seule entrée.

KMAC est disponible sur Linux avec OpenSSL 3.0 ou ultérieur, et sur Windows 11 Build 26016 ou ultérieur. Vous pouvez utiliser la propriété statique IsSupported pour déterminer si la plateforme prend en charge l’algorithme souhaité.

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

Réflexion

Dans les versions de .NET Core et .NET 5-8, la prise en charge de la création d’un assembly et l’émission de métadonnées de réflexion pour les types créés dynamiquement était limitée à un AssemblyBuilder exécutable. L’absence de prise en charge de l’enregistrement d’un assembly était souvent un facteur bloquant pour les clients migrant de .NET Framework vers .NET. .NET 9 ajoute des API publiques à AssemblyBuilder pour enregistrer un assembly émis.

La nouvelle implémentation persistante de AssemblyBuilder est indépendante du runtime et de la plateforme. Pour créer une instance persistante de AssemblyBuilder, utilisez la nouvelle API AssemblyBuilder.DefinePersistedAssembly. L’API AssemblyBuilder.DefineDynamicAssembly existante accepte le nom de l’assembly et des attributs personnalisés facultatifs. Pour utiliser la nouvelle API, passez l’assembly principal, System.Private.CoreLib, qui est utilisé pour référencer les types de runtime de base. Il n’y a pas d’option pour AssemblyBuilderAccess. Pour l’instant, l’implémentation persistante de AssemblyBuilder prend en charge seulement l’enregistrement, mais pas l’exécution. Après avoir créé une instance du AssemblyBuilder enregistré, les étapes suivantes pour définir un module, un type, une méthode ou une énumération, pour écrire du langage intermédiaire (IL) restent inchangées, ainsi que toutes les autres utilisations. Cela signifie que vous pouvez utiliser du code System.Reflection.Emit existant tel quel pour enregistrer l’assembly. Le code ci-après présente un exemple.

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

Performances

.NET 9 intègre les améliorations apportées au compilateur JIT 64 bits qui visent à améliorer le niveau de performance des applications. Voici quelques-unes des améliorations apportées au compilateur :

La vectorisation Arm64 est une autre nouveauté du runtime.

Optimisations pour les boucles

L’amélioration de la génération de code pour les boucles est une priorité pour .NET 9, et le compilateur 64 bits présente une nouvelle optimisation appelée extension de variable d’induction.

Une variable d’induction est une variable dont la valeur change à mesure que la boucle contenante est itérée. Dans la boucle for suivante, i est une variable d’induction : for (int i = 0; i < 10; i++). Si le compilateur peut analyser l’évolution de la valeur d’une variable d’induction pendant les itérations de sa boucle, il peut produire du code plus performant pour les expressions associées.

Examinez l’exemple suivant qui itère au sein d’un tableau :

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

    return sum;
}

La variable d’index, i, présente une taille de 4 octets. Au niveau de l’assembly, des registres de 64 bits sont généralement utilisés pour contenir les index de tableau sur x64 et, dans les versions précédentes de .NET, le compilateur générait du code qui étendait i à 8 octets avec des zéros pour l’accès au tableau, mais continuait ailleurs de traiter i comme un entier de 4 octets. Cependant, l’extension de i à 8 octets nécessite une instruction supplémentaire sur x64. Avec l’extension de variable d’induction, le compilateur JIT 64 bits étend désormais i à 8 octets pendant toute la boucle, omettant l’extension à base de zéros. L’exécution de boucles sur les tableaux est très courante, et les avantages de cette suppression d’instruction s’ajoutent rapidement.

Améliorations de l’incorporation pour Native AOT

L’un des objectifs de .NET concernant l’incorporateur du compilateur JIT 64 bits est de supprimer autant de restrictions que possible qui empêchent l’incorporation d’une méthode. .NET 9 permet l’incorporation de différents accès aux statiques locales des threads sur Windows x64, Linux x64 et Linux Arm64.

Pour les membres de la classe static, il existe exactement une instance d’un membre dans toutes les instances de la classe, qui « partagent » le membre. Si la valeur d’un membre static est unique à chaque thread, faire de cette valeur une valeur locale de thread peut améliorer le niveau de performance, car il n’est plus nécessaire d’utiliser une primitive d’accès concurrentiel pour accéder en toute sécurité au membre static à partir de son thread contenant.

Auparavant, les accès aux statiques locales des threads dans les programmes compilés par Native AOT imposaient au compilateur JIT 64 bits d’émettre un appel dans le runtime pour obtenir l’adresse de base du stockage local des threads. À présent, le compilateur peut incorporer ces appels, ce qui fait que l’accès à ces données nécessite beaucoup moins d’instructions.

Améliorations de l’optimisation PGO : vérifications de type et casts

Dans .NET 8, l’optimisation guidée par profil dynamique (PGO) était activée par défaut. NET 9 développe l’implémentation PGO du compilateur JIT 64 bits pour profiler davantage de modèles de code. Lorsque la compilation hiérarchisée est activée, le compilateur JIT 64 bits insère déjà l’instrumentation dans votre programme pour profiler son comportement. Lorsqu’il recompile avec les optimisations, le compilateur se sert du profil qu’il a créé au moment de l’exécution pour prendre des décisions spécifiques en fonction de l’exécution actuelle de votre programme. Dans .NET 9, le compilateur JIT 64 bits utilise les données PGO pour améliorer le niveau de performance des vérifications de type.

La détermination du type d’un objet passe par un appel dans le runtime, ce qui grève le niveau de performance. Lorsque le type d’un objet a besoin d’être vérifié, le compilateur JIT 64 bits émet cet appel dans un souci de précision (les compilateurs ne peuvent généralement pas exclure les possibilités, même si elles semblent improbables). Toutefois, si les données PGO suggèrent qu’un objet est susceptible d’être un type spécifique, le compilateur JIT 64 bits émet désormais un chemin rapide pour rechercher ce type à peu de frais, et n’a recours à un chemin d’appel lent dans le runtime qu’en cas de nécessité.

Vectorisation Arm64 dans les bibliothèques .NET

Une nouvelle implémentation de EncodeToUtf8 tire parti de la capacité du compilateur JIT 64 bits à émettre des instructions de chargement/stockage multiregistres sur Arm64. Ce comportement permet aux programmes de traiter des blocs de données plus volumineux avec moins d’instructions. Les applications .NET dans divers domaines devraient bénéficier d’améliorations en termes de rendement sur le matériel Arm64 qui prend en charge ces fonctionnalités. Certains benchmarks réduisent leur temps d’exécution de plus de la moitié.

Kit de développement logiciel (SDK) .NET

Test unitaire

Cette section décrit les mises à jour dont ont fait l’objet les tests unitaires dans .NET 9 : exécution de tests en parallèle et sortie des tests de l’enregistreur d’événements de terminal.

Exécuter des tests en parallèle

Dans .NET 9, dotnet test est mieux intégré à MSBuild. Étant donné que MSBuild prend en charge la génération en parallèle, vous pouvez exécuter des tests en parallèle pour un même projet sur différentes versions cibles de .Net Framework. Par défaut, MSBuild limite le nombre de processus parallèles au nombre de processeurs présents sur l’ordinateur. Vous pouvez également définir votre propre limite à l’aide du switch -maxcpucount. Si vous souhaitez désactiver le parallélisme, définissez la propriété MSBuild TestTfmsInParallel sur false.

Affichage des tests de l’enregistreur d’événements de terminal

La génération de rapports de résultat de test pour dotnet test est désormais prise en charge directement dans l’enregistreur d’événements de terminal MSBuild. Vous bénéficiez de rapports de test plus complets pendant l’exécution des tests (affichage du nom du test en cours d’exécution) et après celle-ci (les éventuelles erreurs de test sont rendues de manière plus efficace).

Pour plus d’informations sur l’enregistreur d’événements de terminal, consultez les options de dotnet build.

Restauration par progression des outils .NET

Les outils .NET sont des applications dépendantes du framework que vous pouvez installer globalement ou localement et exécuter à l’aide du Kit de développement logiciel (SDK) .NET et des runtimes .NET installés. Comme toutes les applications .NET, ces outils ciblent une version majeure spécifique de .NET. Par défaut, les applications ne s’exécutent pas sur les versions récentes de .NET. Les auteurs d’outils peuvent opter pour l’exécution de leurs outils sur les versions récentes du runtime .NET en définissant la propriété MSBuild RollForward. Cependant, tous les outils ne le permettent pas.

Une nouvelle option pour dotnet tool install permet aux utilisateurs de décider de la façon dont les outils .NET doivent s’exécuter. Lorsque vous installez un outil via dotnet tool install ou que vous l’exécutez via dotnet tool run <toolname>, vous pouvez spécifier un nouvel indicateur appelé --allow-roll-forward. Cette option configure l’outil avec le mode de restauration par progression Major. Ce mode permet à l’outil de s’exécuter sur une version majeure plus récente de .NET si la version correspondante de .NET n’est pas disponible. Cette fonctionnalité permet aux utilisateurs précoces d’utiliser les outils .NET sans que les auteurs d’outils n’aient besoin de modifier le moindre code.

Voir aussi