Nouveautés du runtime .NET 8

Cet article décrit les nouvelles fonctionnalités du runtime .NET pour .NET 8.

Optimisation des performances

.NET 8 inclut des améliorations apportées à la génération de code et à la compilation juste-à-temps (JIT) :

  • Améliorations des performances d’Arm64
  • Améliorations SIMD
  • Prise en charge des extensions AVX-512 ISA (voir Vector512 et AVX-512)
  • Améliorations du natif Cloud
  • Améliorations du débit JIT
  • Optimisations générales et des boucles
  • Accès optimisé pour les champs marqués avec ThreadStaticAttribute
  • Allocation de registre consécutive. Arm64 propose deux instructions pour la recherche de vecteurs de table, qui exigent que toutes les entités de leurs opérandes tuples soient présentes dans des registres consécutifs.
  • JIT/NativeAOT peut désormais dérouler et vectoriser automatiquement certaines opérations de mémoire avec SIMD, telles que la comparaison, la copie et la mise à zéro, s’il peut déterminer leurs tailles au moment de la compilation.

Par ailleurs, l’optimisation dynamique guidée par profil (PGO) a été améliorée et est désormais activée par défaut. Vous n’avez plus besoin d’utiliser une option de configuration d’exécution pour l’activer. L’optimisation guidée par profil dynamique fonctionne main dans la main avec la compilation hiérarchisée pour optimiser davantage le code en fonction de l’instrumentation supplémentaire mise en place pendant le niveau 0.

En moyenne, la PGO dynamique augmente les performances d’environ 15 %. Dans une série d’environ 4 600 tests, 23 % des tests ont vu des améliorations de performances de l’ordre de 20 % ou plus.

Promotion du struct Codegen

.NET 8 inclut un nouveau passe d’optimisation de la promotion physique pour codegen qui généralise la capacité du JIT à promouvoir les variables de struct. Cette optimisation (également appelée remplacement scalaire des agrégats) remplace les champs de variables de struct par des variables primitives que le JIT peut ensuite inclure dans son raisonnement et optimiser plus précisément.

Le JIT prenait déjà en charge cette optimisation, mais avec plusieurs limitations importantes, notamment :

  • Elle n’était prise en charge que pour les structs avec quatre champs ou moins.
  • Elle n’était prise en charge que si chaque champ était un type primitif ou un struct simple encapsulant un type primitif.

La promotion physique supprime ces limitations, ce qui résout un certain nombre de problèmes JIT historiques.

Nettoyage de la mémoire

.NET 8 ajoute une fonctionnalité permettant d’ajuster la limite de mémoire à la volée. Cela est utile dans les scénarios de service cloud, où la demande vient et va. Pour être rentables, les services doivent être mis à l’échelle en fonction de la consommation des ressources à mesure que la demande fluctue. Lorsqu’un service détecte une diminution de la demande, il peut réduire la consommation de ressources en réduisant sa limite de mémoire. Auparavant, cela échouait, car le récupérateur de mémoire (GC) ignorait la modification et pouvait allouer plus de mémoire que la nouvelle limite. Avec cette modification, vous pouvez appeler l’API RefreshMemoryLimit() pour mettre à jour le GC avec la nouvelle limite de mémoire.

Il existe certaines limites à connaître :

  • Sur les plateformes 32 bits (par exemple, Windows x86 et Linux ARM), .NET n’est pas en mesure d’établir une nouvelle limite matérielle de tas s’il n’en existe pas déjà une.
  • L’API peut retourner un code d’état différent de zéro indiquant l’échec de l’actualisation. Cela peut se produire si le scale-down est trop agressif et ne laisse aucune marge de manœuvre au GC. Dans ce cas, envisagez d’appeler GC.Collect(2, GCCollectionMode.Aggressive) pour réduire l’utilisation actuelle de la mémoire, puis réessayez.
  • Si vous augmentez la limite de mémoire au-delà de la taille que le GC croit que le processus peut gérer au démarrage, l’appel RefreshMemoryLimit réussit, mais il ne pourra pas utiliser plus de mémoire que ce qu’il perçoit comme limite.

L’extrait de code suivant montre comment nommer l’API.

GC.RefreshMemoryLimit();

Vous pouvez également actualiser certains des paramètres de configuration GC liés à la limite de mémoire. L’extrait de code suivant définit la limite matérielle du tas à 100 miooctets (Mio) :

AppContext.SetData("GCHeapHardLimit", (ulong)100 * 1_024 * 1_024);
GC.RefreshMemoryLimit();

L’API peut lever une InvalidOperationException valeur si la limite matérielle n’est pas valide, par exemple, dans le cas de pourcentages de limite de tas négatifs et si la limite matérielle est trop faible. Cela peut se produire, si la limite matérielle du tas définie par l’actualisation, due à de nouveaux paramètres AppData ou engendrée par les modifications de la limite de mémoire du conteneur, est inférieure à ce qui est déjà validé.

Globalisation pour les applications mobiles

Les applications mobiles sur iOS, tvOS et MacCatalyst peuvent choisir un nouveau mode de globalisation hybride qui utilise un pack ICU plus léger. En mode hybride, les données de globalisation sont partiellement extraites du pack ICU et partiellement des appels dans l’API native. Le mode hybride sert tous les paramètres régionaux pris en charge par les mobiles.

Le mode hybride convient mieux aux applications qui ne peuvent pas fonctionner en mode de globalisation invariant et qui utilisent des cultures supprimées des données d’ICU sur les mobiles. Vous pouvez également l’utiliser lorsque vous souhaitez charger un fichier de données ICU plus petit. (Le fichier icudt_hybrid.dat est plus petit de 34,5 % que le fichier de données d’ICU par défaut icudt.dat.)

Pour utiliser le mode de globalisation hybride, définissez la propriété MSBuild HybridGlobalization sur true :

<PropertyGroup>
  <HybridGlobalization>true</HybridGlobalization>
</PropertyGroup>

Il existe certaines limites à connaître, comme les suivantes :

  • En raison des limitations de l’API native, toutes les API de globalisation ne sont pas prises en charge en mode hybride.
  • Certaines API prises en charge ont un comportement différent.

Pour vérifier si votre application est affectée, consultez Différences de comportement.

COM Interop générée par la source

.NET 8 inclut un nouveau générateur source qui prend en charge l’interopérabilité avec les interfaces COM. Vous pouvez utiliser GeneratedComInterfaceAttribute pour marquer une interface en tant qu’interface COM pour le générateur source. Le générateur source génère ensuite du code pour activer l’appel du code C# vers du code non managé. Il génère également du code pour activer l’appel de code non managé en C#. Ce générateur source s’intègre à LibraryImportAttribute, et vous pouvez utiliser des types avec GeneratedComInterfaceAttribute comme paramètres et retourner des types dans les méthodes avec les attributs de LibraryImport.

using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;

[GeneratedComInterface]
[Guid("5401c312-ab23-4dd3-aa40-3cb4b3a4683e")]
partial interface IComInterface
{
    void DoWork();
}

internal partial class MyNativeLib
{
    [LibraryImport(nameof(MyNativeLib))]
    public static partial void GetComInterface(out IComInterface comInterface);
}

Le générateur source prend également en charge le nouvel attribut GeneratedComClassAttribute pour vous permettre de passer des types qui implémentent des interfaces avec l’attribut GeneratedComInterfaceAttribute à du code non managé. Le générateur source génère le code nécessaire pour exposer un objet COM qui implémente les interfaces et transfère les appels à l’implémentation managée.

Les méthodes sur les interfaces avec l’attribut GeneratedComInterfaceAttribute prennent en charge tous les mêmes types que LibraryImportAttribute, et LibraryImportAttribute prend désormais en charge les types avec les attributs GeneratedComInterface et GeneratedComClass.

Si votre code C# utilise uniquement une interface avec les attributs GeneratedComInterface pour encapsuler un objet COM à partir de code non managé ou encapsuler un objet managé à partir de C# pour l’exposer à du code non managé, vous pouvez utiliser les options de la propriété Options pour personnaliser le code qui sera généré. Ces options signifient que vous n’avez pas besoin d’écrire des marshallers pour les scénarios que vous savez ne seront pas utilisés.

Le générateur source utilise le nouveau type StrategyBasedComWrappers pour créer et gérer les wrappers d’objets COM et les wrappers d’objets managés. Ce nouveau type gère l’expérience utilisateur .NET attendue pour COM Interop, tout en fournissant des points de personnalisation pour les utilisateurs avancés. Si votre application a son propre mécanisme pour définir des types à partir de COM ou si vous devez prendre en charge des scénarios que le COM généré par la source ne prend actuellement pas en charge, envisagez d’utiliser le nouveau type StrategyBasedComWrappers pour ajouter les fonctionnalités manquantes pour votre scénario et obtenir la même expérience utilisateur .NET pour vos types COM.

Si vous utilisez Visual Studio, les nouveaux analyseurs et correctifs de code facilitent la conversion de votre code COM Interop existant pour utiliser l’interopérabilité générée par la source. À côté de chaque interface qui a ComImportAttribute, une ampoule offre la possibilité de conversion en interopérabilité générée par la source. Le correctif modifie l’interface pour utiliser l’attribut GeneratedComInterfaceAttribute. À côté de chaque classe qui implémente une interface avec GeneratedComInterfaceAttribute, une ampoule offre une option permettant d’ajouter l’attribut GeneratedComClassAttribute au type. Une fois vos types convertis, vous pouvez déplacer vos méthodes DllImport pour utiliser LibraryImportAttribute.

Limites

Le générateur de source COM ne prend pas en charge l’affinité d’appartement, en utilisant le mot clé new pour activer une coclasse COM et les API suivantes :

  • Interfaces basées sur IDispatch.
  • Interfaces basées sur IInspectable.
  • Propriétés et événements COM.

Générateur de source de liaison de configuration

.NET 8 introduit un générateur de source pour fournir une configuration AOT compatible avec la découpe dans ASP.NET Core. Le générateur est une alternative à l’implémentation basée sur la réflexion préexistante.

Le générateur source sonde les appels Configure(TOptions), Bind et Get pour récupérer les informations de type à partir de ces derniers. Lorsque le générateur est activé dans un projet, le compilateur choisit implicitement les méthodes générées sur les implémentations d’infrastructure basées sur la réflexion préexistantes.

Aucune modification du code source n’est nécessaire pour utiliser le générateur. Il est activé par défaut dans les applications web AOT. Pour les autres types de projets, le générateur source est désactivé par défaut, mais vous pouvez vous inscrire en définissant la propriété EnableConfigurationBindingGenerator sur true dans votre fichier projet :

<PropertyGroup>
    <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>

Le code suivant montre un exemple d’appel du binder.

public class ConfigBindingSG
{
    static void RunIt(params string[] args)
    {
        WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
        IConfigurationSection section = builder.Configuration.GetSection("MyOptions");

        // !! Configure call - to be replaced with source-gen'd implementation
        builder.Services.Configure<MyOptions>(section);

        // !! Get call - to be replaced with source-gen'd implementation
        MyOptions? options0 = section.Get<MyOptions>();

        // !! Bind call - to be replaced with source-gen'd implementation
        MyOptions options1 = new();
        section.Bind(options1);

        WebApplication app = builder.Build();
        app.MapGet("/", () => "Hello World!");
        app.Run();
    }

    public class MyOptions
    {
        public int A { get; set; }
        public string S { get; set; }
        public byte[] Data { get; set; }
        public Dictionary<string, string> Values { get; set; }
        public List<MyClass> Values2 { get; set; }
    }

    public class MyClass
    {
        public int SomethingElse { get; set; }
    }
}

Bibliothèques .NET Core

Cette section comprend les sous-rubriques suivantes :

Réflexion

Les pointeurs de fonction ont été introduits dans .NET 5, mais la prise en charge correspondante de la réflexion n’a pas été ajoutée à ce moment-là. Quand vous utilisiez typeof ou la réflexion sur un pointeur de fonction, par exemple typeof(delegate*<void>()) ou FieldInfo.FieldType respectivement, un IntPtr était retourné. À partir de .NET 8, un objet System.Type est retourné à la place. Ce type permet d’accéder aux métadonnées du pointeur de fonction, notamment les conventions d’appel, le type de retour et les paramètres.

Notes

Une instance de pointeur de fonction, qui est une adresse physique à une fonction, continue d’être représentée sous la forme d’un IntPtr. Seul le type de réflexion a changé.

Pour le moment, la nouvelle fonctionnalité est uniquement implémentée dans le runtime CoreCLR et MetadataLoadContext.

De nouvelles API ont été ajoutées à System.Type, telles que IsFunctionPointer, et à System.Reflection.PropertyInfo, System.Reflection.FieldInfo et System.Reflection.ParameterInfo. Le code suivant montre comment utiliser quelques-unes des nouvelles API pour la réflexion.

using System;
using System.Reflection;

// Sample class that contains a function pointer field.
public unsafe class UClass
{
    public delegate* unmanaged[Cdecl, SuppressGCTransition]<in int, void> _fp;
}

internal class FunctionPointerReflection
{
    public static void RunIt()
    {
        FieldInfo? fieldInfo = typeof(UClass).GetField(nameof(UClass._fp));

        // Obtain the function pointer type from a field.
        Type? fpType = fieldInfo?.FieldType;

        // New methods to determine if a type is a function pointer.
        Console.WriteLine(
        $"IsFunctionPointer: {fpType?.IsFunctionPointer}");
        Console.WriteLine(
            $"IsUnmanagedFunctionPointer: {fpType?.IsUnmanagedFunctionPointer}");

        // New methods to obtain the return and parameter types.
        Console.WriteLine($"Return type: {fpType?.GetFunctionPointerReturnType()}");

        if (fpType is not null)
        {
            foreach (Type parameterType in fpType.GetFunctionPointerParameterTypes())
            {
                Console.WriteLine($"Parameter type: {parameterType}");
            }
        }

        // Access to custom modifiers and calling conventions requires a "modified type".
        Type? modifiedType = fieldInfo?.GetModifiedFieldType();

        // A modified type forwards most members to its underlying type.
        Type? normalType = modifiedType?.UnderlyingSystemType;

        if (modifiedType is not null)
        {
            // New method to obtain the calling conventions.
            foreach (Type callConv in modifiedType.GetFunctionPointerCallingConventions())
            {
                Console.WriteLine($"Calling convention: {callConv}");
            }
        }

        // New method to obtain the custom modifiers.
        Type[]? modifiers =
            modifiedType?.GetFunctionPointerParameterTypes()[0].GetRequiredCustomModifiers();

        if (modifiers is not null)
        {
            foreach (Type modreq in modifiers)
            {
                Console.WriteLine($"Required modifier for first parameter: {modreq}");
            }
        }
    }
}

L’exemple précédent génère la sortie suivante :

IsFunctionPointer: True
IsUnmanagedFunctionPointer: True
Return type: System.Void
Parameter type: System.Int32&
Calling convention: System.Runtime.CompilerServices.CallConvSuppressGCTransition
Calling convention: System.Runtime.CompilerServices.CallConvCdecl
Required modifier for first parameter: System.Runtime.InteropServices.InAttribute

Sérialisation

Diverses améliorations ont été apportées à la fonctionnalité de sérialisation et de désérialisation System.Text.Json dans .NET 8. Par exemple, vous pouvez personnaliser la gestion des membres qui ne figurent pas dans la charge utile JSON.

Les sections suivantes décrivent d’autres améliorations de sérialisation :

Pour plus d’informations sur la sérialisation JSON en général, consultez Sérialisation et désérialisation JSON dans .NET.

Prise en charge intégrée de types supplémentaires

Le sérialiseur prend en charge les types supplémentaires suivants.

  • Types numériques Half, Int128 et UInt128.

    Console.WriteLine(JsonSerializer.Serialize(
        [ Half.MaxValue, Int128.MaxValue, UInt128.MaxValue ]
    ));
    // [65500,170141183460469231731687303715884105727,340282366920938463463374607431768211455]
    
  • Valeurs Memory<T> et ReadOnlyMemory<T>. byte valeurs sont sérialisées en chaînes Base64 et d’autres types dans des tableaux JSON.

    JsonSerializer.Serialize<ReadOnlyMemory<byte>>(new byte[] { 1, 2, 3 }); // "AQID"
    JsonSerializer.Serialize<Memory<int>>(new int[] { 1, 2, 3 }); // [1,2,3]
    

Générateur de source

.NET 8 inclut des améliorations du générateur source System.Text.Json qui visent à rendre l’expérience AOT native à l’égal du sérialiseur basé sur la réflexion. Par exemple :

  • Le générateur de source prend désormais en charge la sérialisation des types avec les propriétés required et init. Ces deux éléments étaient déjà pris en charge dans la sérialisation basée sur la réflexion.

  • Amélioration de la mise en forme du code généré par la source.

  • JsonSourceGenerationOptionsAttribute parité des fonctionnalités avec JsonSerializerOptions. Pour plus d’informations, consultez Spécifier les options (génération de source).

  • Diagnostics supplémentaires (tels que SYSLIB1034 et SYSLIB1039).

  • N’incluez pas les types de propriétés ignorées ou inaccessibles.

  • Prend en charge l’imbrication de déclarations JsonSerializerContext dans des types arbitraires.

  • Prend en charge des types générés par le compilateur ou indicibles dans les scénarios de génération de sources faiblement typées. Étant donné que les types générés par le compilateur ne peuvent pas être spécifiés explicitement par le générateur source, System.Text.Json effectue désormais la résolution ancêtre la plus proche au moment de l’exécution. Cette résolution détermine le supertype le plus approprié avec lequel sérialiser la valeur.

  • Nouveau type de convertisseur JsonStringEnumConverter<TEnum>. La classe existante JsonStringEnumConverter n’est pas prise en charge par l’AOA natif. Vous pouvez annoter vos types enum comme suit :

    [JsonConverter(typeof(JsonStringEnumConverter<MyEnum>))]
    public enum MyEnum { Value1, Value2, Value3 }
    
    [JsonSerializable(typeof(MyEnum))]
    public partial class MyContext : JsonSerializerContext { }
    

    Pour plus d’informations, consultez Sérialiser des champs d’énumération sous forme de chaînes.

  • La nouvelle propriété JsonConverter.Type vous permet de rechercher le type d’une instance JsonConverter non générique :

    Dictionary<Type, JsonConverter> CreateDictionary(IEnumerable<JsonConverter> converters)
        => converters.Where(converter => converter.Type != null)
                     .ToDictionary(converter => converter.Type!);
    

    La propriété peut accepter la valeur Null, car elle retourne null pour les instances JsonConverterFactory et typeof(T) pour les instances JsonConverter<T>.

Générateurs de sources de chaîne

La classe JsonSerializerOptions inclut une nouvelle propriété TypeInfoResolverChain qui complète la propriété TypeInfoResolver existante. Ces propriétés sont utilisées dans la personnalisation des contrats pour les générateurs sources de chaînage. L’ajout de la nouvelle propriété signifie que vous n’avez pas besoin de spécifier tous les composants chaînés sur un site d’appel. Ils peuvent être ajoutés après coup. TypeInfoResolverChain vous permet également d’introspecter la chaîne ou d’en supprimer des composants. Pour plus d’informations, consultez Combiner des générateurs de source.

En outre, JsonSerializerOptions.AddContext<TContext>() est désormais obsolète. Elle a été remplacée par les propriétés TypeInfoResolver et TypeInfoResolverChain. Pour plus d’informations, consultez SYSLIB0049.

Hiérarchies d’interface

.NET 8 ajoute la prise en charge de la sérialisation des propriétés à partir des hiérarchies d’interface.

Le code suivant montre un exemple où les propriétés de l’interface implémentée immédiatement et de son interface de base sont sérialisées.

public static void InterfaceHierarchies()
{
    IDerived value = new DerivedImplement { Base = 0, Derived = 1 };
    string json = JsonSerializer.Serialize(value);
    Console.WriteLine(json); // {"Derived":1,"Base":0}
}

public interface IBase
{
    public int Base { get; set; }
}

public interface IDerived : IBase
{
    public int Derived { get; set; }
}

public class DerivedImplement : IDerived
{
    public int Base { get; set; }
    public int Derived { get; set; }
}

Stratégies d’affectation de noms

JsonNamingPolicy inclut de nouvelles stratégies de nommage pour les conversions de noms de propriétés snake_case (avec trait de soulignement) et kebab-case (avec trait d’union). Utilisez ces stratégies de la même façon que la stratégie JsonNamingPolicy.CamelCase existante :

var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
JsonSerializer.Serialize(new { PropertyName = "value" }, options);
// { "property_name" : "value" }

Pour plus d’informations, consultez Utiliser une stratégie d’attribution de noms intégrée.

Propriétés en lecture seule

Vous pouvez désormais désérialiser sur des champs ou des propriétés en lecture seule (c’est-à-dire ceux qui n’ont pas d’accesseur set).

Pour choisir cette prise en charge globalement, définissez une nouvelle option, PreferredObjectCreationHandling, sur JsonObjectCreationHandling.Populate. Si la compatibilité vous préoccupe, vous pouvez également activer la fonctionnalité de manière plus granulaire en plaçant l’attribut [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] sur les types spécifiques dont les propriétés doivent être remplies ou sur des propriétés individuelles.

Par exemple, considérez le code suivant qui désérialise dans un type CustomerInfo qui a deux propriétés en lecture seule.

public static void ReadOnlyProperties()
{
    CustomerInfo customer = JsonSerializer.Deserialize<CustomerInfo>("""
        { "Names":["John Doe"], "Company":{"Name":"Contoso"} }
        """)!;

    Console.WriteLine(JsonSerializer.Serialize(customer));
}

class CompanyInfo
{
    public required string Name { get; set; }
    public string? PhoneNumber { get; set; }
}

[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
class CustomerInfo
{
    // Both of these properties are read-only.
    public List<string> Names { get; } = new();
    public CompanyInfo Company { get; } = new()
    {
        Name = "N/A",
        PhoneNumber = "N/A"
    };
}

Avant .NET 8, les valeurs d’entrée ont été ignorées et les propriétés Names et Company ont conservé leurs valeurs par défaut.

{"Names":[],"Company":{"Name":"N/A","PhoneNumber":"N/A"}}

À présent, les valeurs d’entrée sont utilisées pour remplir les propriétés en lecture seule pendant la désérialisation.

{"Names":["John Doe"],"Company":{"Name":"Contoso","PhoneNumber":"N/A"}}

Pour plus d’informations sur le comportement de désérialisation de remplissage, consultez Remplir les propriétés initialisées.

Désactiver la valeur par défaut basée sur la réflexion

Vous pouvez maintenant désactiver l’utilisation du sérialiseur basé sur la réflexion par défaut. Cette désactivation est utile pour éviter l’enracinement accidentel de composants de réflexion qui ne sont même pas en cours d’utilisation, en particulier dans les applications AOA découpées et natives. Pour désactiver la sérialisation basée sur la réflexion par défaut, en exigeant qu’un argument JsonSerializerOptions soit passé aux méthodes de sérialisation et de désérialisation JsonSerializer, définissez la propriété MSBuild JsonSerializerIsReflectionEnabledByDefault sur la valeur false dans votre fichier projet.

Utilisez la nouvelle API IsReflectionEnabledByDefault pour vérifier la valeur du commutateur de fonctionnalité. Si vous êtes un auteur de bibliothèque en plus de System.Text.Json, vous pouvez vous appuyer sur la propriété pour configurer vos valeurs par défaut sans racine accidentelle des composants de réflexion.

Pour plus d’informations, consultez Désactiver la valeur par défaut basée de la réflexion.

Nouvelles méthodes d’API JsonNode

Les types JsonNode et System.Text.Json.Nodes.JsonArray incluent les nouvelles méthodes suivantes.

public partial class JsonNode
{
    // Creates a deep clone of the current node and all its descendants.
    public JsonNode DeepClone();

    // Returns true if the two nodes are equivalent JSON representations.
    public static bool DeepEquals(JsonNode? node1, JsonNode? node2);

    // Determines the JsonValueKind of the current node.
    public JsonValueKind GetValueKind(JsonSerializerOptions options = null);

    // If node is the value of a property in the parent
    // object, returns its name.
    // Throws InvalidOperationException otherwise.
    public string GetPropertyName();

    // If node is the element of a parent JsonArray,
    // returns its index.
    // Throws InvalidOperationException otherwise.
    public int GetElementIndex();

    // Replaces this instance with a new value,
    // updating the parent object/array accordingly.
    public void ReplaceWith<T>(T value);

    // Asynchronously parses a stream as UTF-8 encoded data
    // representing a single JSON value into a JsonNode.
    public static Task<JsonNode?> ParseAsync(
        Stream utf8Json,
        JsonNodeOptions? nodeOptions = null,
        JsonDocumentOptions documentOptions = default,
        CancellationToken cancellationToken = default);
}

public partial class JsonArray
{
    // Returns an IEnumerable<T> view of the current array.
    public IEnumerable<T> GetValues<T>();
}

Membres non publics

Vous pouvez inclure des membres non publics dans le contrat de sérialisation d’un type donné à l’aide d’annotations d’attributs JsonIncludeAttribute et JsonConstructorAttribute.

public static void NonPublicMembers()
{
    string json = JsonSerializer.Serialize(new MyPoco(42));
    Console.WriteLine(json);
    // {"X":42}

    JsonSerializer.Deserialize<MyPoco>(json);
}

public class MyPoco
{
    [JsonConstructor]
    internal MyPoco(int x) => X = x;

    [JsonInclude]
    internal int X { get; }
}

Pour plus d’informations, consultez Utiliser des types immuables et des membres et accesseurs non publics.

API de désérialisation de diffusion en continu

.NET 8 inclut de nouvelles méthodes d’extension de désérialisation de diffusion en continu IAsyncEnumerable<T>, par exemple GetFromJsonAsAsyncEnumerable. Des méthodes similaires existent qui retournent Task<TResult>, par exemple HttpClientJsonExtensions.GetFromJsonAsync. Les nouvelles méthodes d’extension appellent les API de diffusion en continu et retournent IAsyncEnumerable<T>.

Le code suivant montre comment utiliser les nouvelles méthodes d’extension.

public async static void StreamingDeserialization()
{
    const string RequestUri = "https://api.contoso.com/books";
    using var client = new HttpClient();
    IAsyncEnumerable<Book?> books = client.GetFromJsonAsAsyncEnumerable<Book>(RequestUri);

    await foreach (Book? book in books)
    {
        Console.WriteLine($"Read book '{book?.title}'");
    }
}

public record Book(int id, string title, string author, int publishedYear);

Méthode d’extension WithAddedModifier

La nouvelle méthode d’extension WithAddedModifier(IJsonTypeInfoResolver, Action<JsonTypeInfo>) vous permet d’introduire facilement des modifications apportées aux contrats de sérialisation d’instances arbitraires IJsonTypeInfoResolver.

var options = new JsonSerializerOptions
{
    TypeInfoResolver = MyContext.Default
        .WithAddedModifier(static typeInfo =>
        {
            foreach (JsonPropertyInfo prop in typeInfo.Properties)
            {
                prop.Name = prop.Name.ToUpperInvariant();
            }
        })
};

Nouvelles surcharges JsonContent.Create

Vous pouvez désormais créer des instances JsonContent à l’aide de contrats à découpage sécurisé ou générés par la source. Les nouvelles méthodes sont les suivantes :

var book = new Book(id: 42, "Title", "Author", publishedYear: 2023);
HttpContent content = JsonContent.Create(book, MyContext.Default.Book);

public record Book(int id, string title, string author, int publishedYear);

[JsonSerializable(typeof(Book))]
public partial class MyContext : JsonSerializerContext
{
}

Geler une instance JsonSerializerOptions

Les nouvelles méthodes suivantes vous permettent de contrôler quand une instance JsonSerializerOptions est gelée :

  • JsonSerializerOptions.MakeReadOnly()

    Cette surcharge est conçue pour être sécurisée en cas de découpage et génère donc une exception dans les cas où l’instance d’options n’a pas été configurée avec un programme de résolution.

  • JsonSerializerOptions.MakeReadOnly(Boolean)

    Si vous passez true à cette surcharge, elle remplit l’instance d’options avec le programme de résolution de réflexion par défaut s’il en manque un. Cette méthode est marquée RequiresUnreferenceCode/RequiresDynamicCode et n’est donc pas adaptée aux applications AOT natives.

La nouvelle propriété IsReadOnly vous permet de vérifier si l’instance d’options est gelée.

Abstraction temporelle

La nouvelle classe TimeProvider et l’interface de ITimer ajoutent la fonctionnalité d’abstraction temporelle, ce qui vous permet de simuler le temps dans les scénarios de test. En outre, vous pouvez utiliser l’abstraction de temps pour simuler des opérations de Task qui s’appuient sur la progression du temps à l’aide de Task.Delay et de Task.WaitAsync. L’abstraction temporelle prend en charge les opérations de temps essentielles suivantes :

  • Récupérer l’heure locale et UTC
  • Obtenir un horodatage pour mesurer les performances
  • Créer un retardateur

L’extrait de code suivant présente quelques exemples d’utilisation.

// Get system time.
DateTimeOffset utcNow = TimeProvider.System.GetUtcNow();
DateTimeOffset localNow = TimeProvider.System.GetLocalNow();

TimerCallback callback = s => ((State)s!).Signal();

// Create a timer using the time provider.
ITimer timer = _timeProvider.CreateTimer(
    callback, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan);

// Measure a period using the system time provider.
long providerTimestamp1 = TimeProvider.System.GetTimestamp();
long providerTimestamp2 = TimeProvider.System.GetTimestamp();

TimeSpan period = _timeProvider.GetElapsedTime(providerTimestamp1, providerTimestamp2);
// Create a time provider that works with a
// time zone that's different than the local time zone.
private class ZonedTimeProvider(TimeZoneInfo zoneInfo) : TimeProvider()
{
    private readonly TimeZoneInfo _zoneInfo = zoneInfo ?? TimeZoneInfo.Local;

    public override TimeZoneInfo LocalTimeZone => _zoneInfo;

    public static TimeProvider FromLocalTimeZone(TimeZoneInfo zoneInfo) =>
        new ZonedTimeProvider(zoneInfo);
}

Améliorations apportées à UTF8

Si vous souhaitez activer l’écriture d’une représentation de type chaîne de votre type dans une étendue de destination, implémentez la nouvelle interface de IUtf8SpanFormattable sur votre type. Cette nouvelle interface est étroitement liée à ISpanFormattable, mais cible UTF8 et Span<byte> au lieu de UTF16 et Span<char>.

IUtf8SpanFormattable a été implémenté sur tous les types primitifs (plus d’autres), avec la même logique partagée que le ciblage string, Span<char> ou Span<byte>. Il prend entièrement en charge tous les formats (y compris le nouveau spécificateur binaire « B ») et toutes les cultures. Cela signifie que vous pouvez désormais mettre en forme directement en UTF8 à partir de Byte, Complex, Char, DateOnly, DateTime, DateTimeOffset, Decimal, Double, Guid, Half, IPAddress, IPNetwork, Int16, Int32, Int64, Int128, IntPtr, NFloat, SByte, Single, Rune, TimeOnly, TimeSpan, UInt16, UInt32, UInt64, UInt128, UIntPtr, et Version.

Les nouvelles méthodes Utf8.TryWrite fournissent un équivalent basé sur UTF8 aux méthodes MemoryExtensions.TryWrite existantes, qui sont basées sur UTF16. Vous pouvez utiliser la syntaxe de chaîne interpolée pour mettre en forme une expression complexe directement dans une étendue d’octets UTF8, par exemple :

static bool FormatHexVersion(
    short major,
    short minor,
    short build,
    short revision,
    Span<byte> utf8Bytes,
    out int bytesWritten) =>
    Utf8.TryWrite(
        utf8Bytes,
        CultureInfo.InvariantCulture,
        $"{major:X4}.{minor:X4}.{build:X4}.{revision:X4}",
        out bytesWritten);

L’implémentation reconnaît IUtf8SpanFormattable sur les valeurs de format et utilise leurs implémentations pour écrire leurs représentations UTF8 directement dans l’étendue de destination.

L’implémentation utilise également la nouvelle méthode Encoding.TryGetBytes(ReadOnlySpan<Char>, Span<Byte>, Int32), qui, avec son équivalent Encoding.TryGetChars(ReadOnlySpan<Byte>, Span<Char>, Int32), prend en charge l’encodage et le décodage dans une étendue de destination. Si l’étendue n’est pas suffisamment longue pour contenir l’état résultant, les méthodes retournent false plutôt que de lever une exception.

Méthodes pour l’utilisation du caractère aléatoire

Les types System.Random et System.Security.Cryptography.RandomNumberGenerator introduisent deux nouvelles méthodes pour l’utilisation du caractère aléatoire.

GetItems<T>()

Les nouvelles méthodes System.Random.GetItems et System.Security.Cryptography.RandomNumberGenerator.GetItems vous permettent de choisir de manière aléatoire un nombre spécifié d’éléments à partir d’un jeu d’entrée. L’exemple suivant montre comment utiliser System.Random.GetItems<T>() (sur l’instance fournie par la propriété Random.Shared) pour insérer de manière aléatoire 31 éléments dans un tableau. Cet exemple peut être utilisé dans un jeu de type « Simon » où les joueurs doivent se souvenir d’une séquence de boutons colorés.

private static ReadOnlySpan<Button> s_allButtons = new[]
{
    Button.Red,
    Button.Green,
    Button.Blue,
    Button.Yellow,
};

// ...

Button[] thisRound = Random.Shared.GetItems(s_allButtons, 31);
// Rest of game goes here ...

Shuffle<T>()

Les nouvelles méthodes Random.Shuffle et RandomNumberGenerator.Shuffle<T>(Span<T>) vous permettent d’ordonner une étendue de manière aléatoire. Ces méthodes sont utiles pour réduire les biais d’entraînement dans le Machine Learning (la première chose n’est donc pas toujours l’entraînement, et la dernière chose est toujours le test).

YourType[] trainingData = LoadTrainingData();
Random.Shared.Shuffle(trainingData);

IDataView sourceData = mlContext.Data.LoadFromEnumerable(trainingData);

DataOperationsCatalog.TrainTestData split = mlContext.Data.TrainTestSplit(sourceData);
model = chain.Fit(split.TrainSet);

IDataView predictions = model.Transform(split.TestSet);
// ...

Types axés sur les performances

.NET 8 introduit plusieurs nouveaux types visant à améliorer les performances des applications.

  • Le nouvel espace de noms System.Collections.Frozen inclut les types de collection FrozenDictionary<TKey,TValue> et FrozenSet<T>. Ces types n’autorisent aucune modification des clés et des valeurs une fois la collection créée. Cette exigence permet d’accélérer les opérations de lecture (par exemple, TryGetValue()). Ces types sont particulièrement utiles pour les collections qui sont remplies lors de la première utilisation, puis conservées pendant toute la durée d’un service à longue durée de vie, par exemple :

    private static readonly FrozenDictionary<string, bool> s_configurationData =
        LoadConfigurationData().ToFrozenDictionary(optimizeForReads: true);
    
    // ...
    if (s_configurationData.TryGetValue(key, out bool setting) && setting)
    {
        Process();
    }
    
  • Les méthodes comme MemoryExtensions.IndexOfAny recherchent la première occurrence de n’importe quelle valeur dans la collection passée. Le nouveau type System.Buffers.SearchValues<T> est conçu pour être passé à de telles méthodes. En conséquence, .NET 8 ajoute de nouvelles surcharges de méthodes comme MemoryExtensions.IndexOfAny qui acceptent une instance du nouveau type. Lorsque vous créez une instance de SearchValues<T>, toutes les données nécessaires pour optimiser les recherches ultérieures sont dérivées à ce moment-là, de sorte que le travail est effectué à l’avance.

  • Le nouveau type System.Text.CompositeFormat est utile pour optimiser les chaînes de format qui ne sont pas connues au moment de la compilation (par exemple si la chaîne de format est chargée à partir d’un fichier de ressources). Un peu de temps supplémentaire est consacré au départ pour effectuer du travail tel que l’analyse de la chaîne, mais cela évite que le travail ne soit effectué à chaque utilisation.

    private static readonly CompositeFormat s_rangeMessage =
        CompositeFormat.Parse(LoadRangeMessageResource());
    
    // ...
    static string GetMessage(int min, int max) =>
        string.Format(CultureInfo.InvariantCulture, s_rangeMessage, min, max);
    
  • Les nouveaux types System.IO.Hashing.XxHash3 et System.IO.Hashing.XxHash128 fournissent des implémentations des algorithmes de hachage rapides XXH3 et XXH128.

System.Numerics et System.Runtime.Intrinsics

Cette section traite des améliorations apportées aux espaces de noms System.Numerics et System.Runtime.Intrinsics.

  • Vector256<T>, Matrix3x2 et Matrix4x4 offrent une amélioration de l’accélération matérielle sur .NET 8. Par exemple, Vector256<T> a été réimplémenté de façon à être des opérations 2x Vector128<T> en interne, si possible. Cela permet l’accélération partielle de certaines fonctions lorsque Vector128.IsHardwareAccelerated == true mais Vector256.IsHardwareAccelerated == false, par exemple sur Arm64.
  • Les intrinsèques matérielles sont désormais annotées avec l’attribut ConstExpected. Cela garantit que les utilisateurs savent quand le matériel sous-jacent attend une constante, et par conséquent quand une valeur non constante peut nuire de manière inattendue aux performances.
  • L’API Lerp(TSelf, TSelf, TSelf)Lerp a été ajoutée à IFloatingPointIeee754<TSelf> et par conséquent à float (Single), double (Double) et Half. Cette API permet d’effectuer efficacement et correctement une interpolation linéaire entre deux valeurs.

Vector512 et AVX-512

La prise en charge SIMD étendue de .NET Core 3.0 pour inclure les API d’intrinsèques matérielles spécifiques à la plateforme pour x86/x64. .NET 5 a ajouté la prise en charge d’Arm64 et .NET 7 à l’intrinsèque matériel multiplateforme. .NET 8 renforce la prise en charge de SIMD en introduisant Vector512<T> et en prenant en charge les instructions d’Intel Advanced Vector Extensions 512 (AVX-512).

Plus précisément, .NET 8 inclut la prise en charge des fonctionnalités clés suivantes d’AVX-512 :

  • Opérations vectorielles 512 bits
  • 16 registres SIMD supplémentaires
  • Instructions supplémentaires disponibles pour les vecteurs 128 bits, 256 bits et 512 bits

Si vous avez du matériel qui prend en charge les fonctionnalités, Vector512.IsHardwareAccelerated signale désormais true.

.NET 8 ajoute également plusieurs classes spécifiques à la plateforme sous l’espace de noms System.Runtime.Intrinsics.X86 :

Ces classes suivent la même forme générale que les autres architectures de jeu d’instructions (ISA) dans lesquelles elles exposent une propriété IsSupported et une classe de Avx512F.X64 imbriquée pour obtenir des instructions disponibles uniquement pour les processus 64 bits. En outre, chaque classe a une classe Avx512F.VL imbriquée qui expose les extensions Avx512VL (longueur vectorielle) pour le jeu d’instructions correspondant.

Même si vous n'utilisez pas explicitement d'instructions Vector512 ou Avx512F dans votre code, vous bénéficierez probablement de la nouvelle prise en charge de l'AVX-512. Le JIT peut tirer parti des registres et instructions supplémentaires implicitement lors de l’utilisation Vector128<T> ou Vector256<T>. La bibliothèque de classes de base utilise ces intrinsèques matérielles en interne dans la plupart des opérations exposées par Span<T> et ReadOnlySpan<T> dans de nombreuses API mathématiques exposées pour les types primitifs.

Validation des données

L’espace de noms System.ComponentModel.DataAnnotations inclut de nouveaux attributs de validation des données destinés aux scénarios de validation dans les services natifs cloud. Si les validateurs DataAnnotations préexistants sont destinés à la validation traditionnelle de l’entrée de données dans l’interface utilisateur, comme les champs d’un formulaire, les nouveaux attributs sont conçus pour valider des données qui ne sont pas entrées par l’utilisateur, comme les options de configuration. En plus des nouveaux attributs, de nouvelles propriétés ont été ajoutées aux types RangeAttribute et RequiredAttribute.

Nouvelle API Description
RangeAttribute.MinimumIsExclusive
RangeAttribute.MaximumIsExclusive
Spécifie si les limites sont incluses dans la plage autorisée.
System.ComponentModel.DataAnnotations.LengthAttribute Spécifie les limites inférieures et supérieures pour des chaînes ou des collections. Par exemple, [Length(10, 20)] spécifie au moins 10 éléments et au plus 20 éléments dans une collection.
System.ComponentModel.DataAnnotations.Base64StringAttribute Vérifie qu’une chaîne est une représentation en Base64 valide.
System.ComponentModel.DataAnnotations.AllowedValuesAttribute
System.ComponentModel.DataAnnotations.DeniedValuesAttribute
Spécifie des listes d’autorisation et des listes d’exclusion, respectivement. Par exemple : [AllowedValues("apple", "banana", "mango")].

Mesures

Les nouvelles API vous permettent d’attacher des balises de paire clé-valeur à des objets Meter et Instrument lorsque vous les créez. Les agrégateurs de mesures de métriques publiées peuvent utiliser les balises pour différencier les valeurs agrégées.

var options = new MeterOptions("name")
{
    Version = "version",
    // Attach these tags to the created meter.
    Tags = new TagList()
    {
        { "MeterKey1", "MeterValue1" },
        { "MeterKey2", "MeterValue2" }
    }
};

Meter meter = meterFactory!.Create(options);

Counter<int> counterInstrument = meter.CreateCounter<int>(
    "counter", null, null, new TagList() { { "counterKey1", "counterValue1" } }
);
counterInstrument.Add(1);

Les nouvelles API sont les suivantes :

Chiffrement

.NET 8 ajoute la prise en charge des primitives de hachage SHA-3. (SHA-3 est actuellement pris en charge par Linux avec OpenSSL 1.1.1 ou version ultérieure et Windows 11 build 25324 ou version ultérieure.) Les API où SHA-2 est disponible offrent désormais un complément SHA-3. Cela inclut SHA3_256, SHA3_384 et SHA3_512 pour le hachage ; HMACSHA3_256, HMACSHA3_384 et HMACSHA3_512 pour HMAC ; HashAlgorithmName.SHA3_256, HashAlgorithmName.SHA3_384 et HashAlgorithmName.SHA3_512 pour le hachage où l’algorithme est configurable ; et RSAEncryptionPadding.OaepSHA3_256, RSAEncryptionPadding.OaepSHA3_384 et RSAEncryptionPadding.OaepSHA3_512 pour le chiffrement RSA OAEP.

L’exemple suivant montre comment utiliser les API, y compris la propriété SHA3_256.IsSupported pour déterminer si la plateforme prend en charge SHA-3.

// Hashing example
if (SHA3_256.IsSupported)
{
    byte[] hash = SHA3_256.HashData(dataToHash);
}
else
{
    // ...
}

// Signing example
if (SHA3_256.IsSupported)
{
     using ECDsa ec = ECDsa.Create(ECCurve.NamedCurves.nistP256);
     byte[] signature = ec.SignData(dataToBeSigned, HashAlgorithmName.SHA3_256);
}
else
{
    // ...
}

La prise en charge de SHA-3 est actuellement destinée à prendre en charge les primitives de chiffrement. Les constructions et les protocoles de niveau supérieur ne sont pas censés prendre entièrement en charge SHA-3 initialement. Ces protocoles incluent les certificats X.509, SignedXml, et COSE.

Réseau

Prise en charge du proxy HTTPS

Jusqu’à présent, les types de proxy que HttpClient prenait en charge ont tous autorisé un « intermédiaire » à voir à quel site le client se connecte, même pour les URI HTTPS. HttpClient prend désormais en charge le proxy HTTPS, qui crée un canal chiffré entre le client et le proxy afin que toutes les requêtes puissent être gérées avec une confidentialité complète.

Pour activer le proxy HTTPS, définissez la variable d’environnement all_proxy ou utilisez la classe WebProxy pour contrôler le proxy par programmation.

Unix : export all_proxy=https://x.x.x.x:3218 Windows : set all_proxy=https://x.x.x.x:3218

Vous pouvez également utiliser la classe WebProxy pour contrôler le proxy par programmation.

Méthodes ZipFile basées sur le flux

.NET 8 inclut de nouvelles surcharges de ZipFile.CreateFromDirectory qui vous permettent de collecter tous les fichiers inclus dans un répertoire et de les compresser, puis de stocker le fichier zip résultant dans le flux fourni. De même, les nouvelles surcharges ZipFile.ExtractToDirectory vous permettent de fournir un flux contenant un fichier compressé et d’extraire son contenu dans le système de fichiers. Voici les nouvelles surcharges :

namespace System.IO.Compression;

public static partial class ZipFile
{
    public static void CreateFromDirectory(
        string sourceDirectoryName, Stream destination);

    public static void CreateFromDirectory(
        string sourceDirectoryName,
        Stream destination,
        CompressionLevel compressionLevel,
        bool includeBaseDirectory);

    public static void CreateFromDirectory(
        string sourceDirectoryName,
        Stream destination,
        CompressionLevel compressionLevel,
        bool includeBaseDirectory,
    Encoding? entryNameEncoding);

    public static void ExtractToDirectory(
        Stream source, string destinationDirectoryName) { }

    public static void ExtractToDirectory(
        Stream source, string destinationDirectoryName, bool overwriteFiles) { }

    public static void ExtractToDirectory(
        Stream source, string destinationDirectoryName, Encoding? entryNameEncoding) { }

    public static void ExtractToDirectory(
        Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles) { }
}

Ces nouvelles API peuvent être utiles lorsque l’espace disque est limité, car elles évitent d’avoir à utiliser le disque comme étape intermédiaire.

Bibliothèques d’extensions

Cette section comprend les sous-rubriques suivantes :

Services d’injection de dépendances (DI) à clé

Les services d’injection de dépendances (DI) à clé fournissent les moyens pour inscrire et récupérer des services d’injection de dépendances en utilisant des clés. En utilisant des clés, vous pouvez mesurez l’étendue de l’inscription et de la consommation de services. Voici quelques-unes des nouvelles API :

L’exemple suivant vous montre comment utiliser des services DI à clé.

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<BigCacheConsumer>();
builder.Services.AddSingleton<SmallCacheConsumer>();
builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
WebApplication app = builder.Build();
app.MapGet("/big", (BigCacheConsumer data) => data.GetData());
app.MapGet("/small", (SmallCacheConsumer data) => data.GetData());
app.MapGet("/big-cache", ([FromKeyedServices("big")] ICache cache) => cache.Get("data"));
app.MapGet("/small-cache", (HttpContext httpContext) => httpContext.RequestServices.GetRequiredKeyedService<ICache>("small").Get("data"));
app.Run();

class BigCacheConsumer([FromKeyedServices("big")] ICache cache)
{
    public object? GetData() => cache.Get("data");
}

class SmallCacheConsumer(IServiceProvider serviceProvider)
{
    public object? GetData() => serviceProvider.GetRequiredKeyedService<ICache>("small").Get("data");
}

public interface ICache
{
    object Get(string key);
}

public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

Pour plus d’informations, consultez dotnet/runtime#64427.

Services de cycle de vie hébergés

Les services hébergés ont désormais davantage d’options d’exécution pendant le cycle de vie de l’application. IHostedService fournissait StartAsync et StopAsync, et maintenant IHostedLifecycleService fournit ces méthodes supplémentaires :

Ces méthodes s’exécutent avant et après les points existants respectivement.

L’exemple suivant montre comment utiliser les nouvelles API.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

internal class HostedLifecycleServices
{
    public async static void RunIt()
    {
        IHostBuilder hostBuilder = new HostBuilder();
        hostBuilder.ConfigureServices(services =>
        {
            services.AddHostedService<MyService>();
        });

        using (IHost host = hostBuilder.Build())
        {
            await host.StartAsync();
        }
    }

    public class MyService : IHostedLifecycleService
    {
        public Task StartingAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StartAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StartedAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StopAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StoppedAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StoppingAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
    }
}

Pour plus d’informations, consultez dotnet/runtime#86511.

Validation des options

Générateur de source

Pour réduire la surcharge au démarrage et améliorer l’ensemble de fonctionnalités de validation, nous avons introduit un générateur de code source qui implémente la logique de validation. Le code suivant montre des exemples de modèles et de classes de validateurs.

public class FirstModelNoNamespace
{
    [Required]
    [MinLength(5)]
    public string P1 { get; set; } = string.Empty;

    [Microsoft.Extensions.Options.ValidateObjectMembers(
        typeof(SecondValidatorNoNamespace))]
    public SecondModelNoNamespace? P2 { get; set; }
}

public class SecondModelNoNamespace
{
    [Required]
    [MinLength(5)]
    public string P4 { get; set; } = string.Empty;
}

[OptionsValidator]
public partial class FirstValidatorNoNamespace
    : IValidateOptions<FirstModelNoNamespace>
{
}

[OptionsValidator]
public partial class SecondValidatorNoNamespace
    : IValidateOptions<SecondModelNoNamespace>
{
}

Si votre application utilise l’injection de dépendances, vous pouvez injecter la validation comme indiqué dans l’exemple de code suivant.

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.Configure<FirstModelNoNamespace>(
    builder.Configuration.GetSection("some string"));

builder.Services.AddSingleton<
    IValidateOptions<FirstModelNoNamespace>, FirstValidatorNoNamespace>();
builder.Services.AddSingleton<
    IValidateOptions<SecondModelNoNamespace>, SecondValidatorNoNamespace>();

Type ValidateOptionsResultBuilder

.NET 8 introduit le type ValidateOptionsResultBuilder pour faciliter la création d’un objet ValidateOptionsResult. Il est important de noter que ce générateur permet l’accumulation de plusieurs erreurs. Auparavant, la création de l’objet ValidateOptionsResult nécessaire à l’implémentation de IValidateOptions<TOptions>.Validate(String, TOptions) était difficile et a parfois entraîné des erreurs de validation en couches. S’il y avait plusieurs erreurs, le processus de validation s’arrêtait souvent à la première erreur.

L’extrait de code suivant affiche un exemple d’utilisation de ValidateOptionsResultBuilder.

ValidateOptionsResultBuilder builder = new();
builder.AddError("Error: invalid operation code");
builder.AddResult(ValidateOptionsResult.Fail("Invalid request parameters"));
builder.AddError("Malformed link", "Url");

// Build ValidateOptionsResult object has accumulating multiple errors.
ValidateOptionsResult result = builder.Build();

// Reset the builder to allow using it in new validation operation.
builder.Clear();

Constructeurs LoggerMessageAttribute

LoggerMessageAttribute offre maintenant des surcharges de constructeur supplémentaires. Auparavant, vous deviez choisir le constructeur sans paramètre ou le constructeur qui nécessitait tous les paramètres (ID d’événement, niveau de journal et message). Les nouvelles surcharges offrent une plus grande flexibilité dans la spécification des paramètres requis avec un code réduit. Si vous ne fournissez pas d’ID d’événement, le système en génère un automatiquement.

public LoggerMessageAttribute(LogLevel level, string message);
public LoggerMessageAttribute(LogLevel level);
public LoggerMessageAttribute(string message);

Métriques d’extensions

Interface IMeterFactory

Vous pouvez inscrire la nouvelle interface IMeterFactory dans des conteneurs d’injection de dépendances (DI) et l’utiliser pour créer des objets Meter de manière isolée.

Inscrivez l’interface IMeterFactory dans le conteneur DI à l’aide de l’implémentation de fabrique de compteur par défaut :

// 'services' is the DI IServiceCollection.
services.AddMetrics();

Les consommateurs peuvent alors obtenir la fabrique de compteur et l’utiliser pour créer un objet Meter.

IMeterFactory meterFactory = serviceProvider.GetRequiredService<IMeterFactory>();

MeterOptions options = new MeterOptions("MeterName")
{
    Version = "version",
};

Meter meter = meterFactory.Create(options);

Classe MetricCollector<T>

La nouvelle classe MetricCollector<T> vous permet d’enregistrer des mesures de métriques ainsi que des horodatages. En outre, la classe offre la possibilité d’utiliser un fournisseur de temps de votre choix pour une génération d’horodatage précise.

const string CounterName = "MyCounter";
DateTimeOffset now = DateTimeOffset.Now;

var timeProvider = new FakeTimeProvider(now);
using var meter = new Meter(Guid.NewGuid().ToString());
Counter<long> counter = meter.CreateCounter<long>(CounterName);
using var collector = new MetricCollector<long>(counter, timeProvider);

Assert.IsNull(collector.LastMeasurement);

counter.Add(3);

// Verify the update was recorded.
Assert.AreEqual(counter, collector.Instrument);
Assert.IsNotNull(collector.LastMeasurement);

Assert.AreSame(collector.GetMeasurementSnapshot().Last(), collector.LastMeasurement);
Assert.AreEqual(3, collector.LastMeasurement.Value);
Assert.AreEqual(now, collector.LastMeasurement.Timestamp);

System.Numerics.Tensors.TensorPrimitives

Le package NuGet System.Numerics.Tensors mis à jour inclut des API dans le nouvel espace de noms TensorPrimitives qui ajoutent la prise en charge des opérations de tenseur. Les primitives de tenseur optimisent les charges de travail gourmandes en données comme celles de l’IA et du Machine Learning.

Les charges de travail d’IA telles que la recherche sémantique et la génération augmentée par récupération (RAG) étendent les fonctionnalités en langage naturel des grands modèles de langage tels que ChatGPT en augmentant les invites avec des données pertinentes. Pour ces charges de travail, les opérations sur les vecteurs, comme la similarité cosinus permettant de trouver les données les plus pertinentes pour répondre à une question, sont cruciales. Le package System.Numerics.Tensors.TensorPrimitives fournit des API pour les opérations vectorielles, ce qui signifie que vous n’avez pas besoin de prendre une dépendance externe ou d’écrire votre propre implémentation.

Ce package remplace le package System.Numerics.Tensors.

Pour plus d’informations, consultez le billet de blog Announcing .NET 8 RC 2.

Voir aussi