Partager via


Nouveautés de ASP.NET Core dans .NET 9

Cet article met en évidence les changements les plus importants dans ASP.NET Core dans .NET 9 avec des liens vers la documentation pertinente.

Optimisation de la distribution des ressources statiques

MapStaticAssets Les conventions de point de terminaison de routage sont une nouvelle fonctionnalité qui optimise la remise des ressources statiques dans les applications ASP.NET Core.

Pour plus d’informations sur la remise de ressources statiques pour les applications Blazor, consultez fichiers statiques Blazor ASP.NET Core.

Le suivi des bonnes pratiques de production pour servir des ressources statiques demande une quantité de travail significative et une grande expertise technique. Sans optimisation telle que la compression, la mise en cache et les empreintes :

  • Le navigateur doit effectuer des requêtes supplémentaires à chaque chargement de page.
  • Plus d’octets que nécessaire sont transférés à travers le réseau.
  • Parfois, des versions obsolètes de fichiers sont servies aux clients.

La création d’applications web performantes nécessite l’optimisation de la remise des ressources au navigateur. Les optimisations possibles sont les suivantes :

  • Servir une seule fois une ressource donnée tant que le fichier ne change pas ou que le navigateur n’efface pas son cache. Définissez l’en-tête ETag.
  • Empêchez le navigateur d’utiliser des ressources anciennes ou obsolètes après la mise à jour d’une application. Définissez l’en-tête Last-Modified.
  • Configurez les en-têtes de mise en cache appropriés.
  • Utilisez un intergiciel de mise en cache.
  • Servez des versions compressées des ressources lorsque cela est possible.
  • Utilisez un CDN pour servir les ressources à proximité de l’utilisateur.
  • Réduire la taille des ressources servies au navigateur. Cette optimisation n’inclut pas la minification.

MapStaticAssets est une nouvelle fonctionnalité qui optimise la livraison des ressources statiques d'une application. Il est conçu pour fonctionner avec toutes les infrastructures d’interface utilisateur, notamment Blazor, Razor Pages et MVC. Il s’agit généralement d’un remplacement direct de UseStaticFiles :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

+app.MapStaticAssets();
-app.UseStaticFiles();
app.MapRazorPages();

app.Run();

MapStaticAssets fonctionne en combinant des processus de génération et de publication pour collecter des informations sur toutes les ressources statiques d’une application. Ces informations sont ensuite utilisées par la bibliothèque runtime pour traiter efficacement ces fichiers dans le navigateur.

MapStaticAssets peut remplacer UseStaticFiles dans la plupart des cas, mais elle est optimisée pour servir les ressources dont l’application a connaissance au moment de la génération et de la publication. Si l’application sert des ressources à partir d’autres emplacements, tels que des ressources sur disque ou incorporées, UseStaticFiles doit être utilisé.

MapStaticAssets offre les avantages suivants non trouvés avec UseStaticFiles:

  • Compression du temps de compilation pour toutes les ressources de l'application :
    • gzip pendant le développement et gzip + brotli pendant la publication.
    • Toutes les ressources sont compressées avec l’objectif de réduire la taille des ressources au minimum.
  • Basé sur le contenu : Les ETags pour chaque ressource sont la chaîne encodée en EtagsBase64 du hachage SHA-256 du contenu. Cela garantit que le navigateur recharge uniquement un fichier si son contenu a changé.

Le tableau suivant montre les tailles d’origine et compressées des fichiers CSS et JS dans le modèle de pages par défaut Razor :

Fichier D’origine Compressé % de réduction
bootstrap.min.css 163 17,5 89,26%
jquery.js 89,6 28 68,75%
bootstrap.min.js 78,5 20 74,52%
Total 331,1 65,5 80,20%

Le tableau suivant présente les tailles d’origine et compressées à l’aide de la bibliothèque de composants Fluent UIBlazor:

Fichier D’origine Compressé % de réduction
fluent.js 384 73 80.99 %
fluent.css 94 11 88,30%
Total 478 84 82,43%

Pour un total de 478 Ko décompressé à 84 Ko compressé.

Le tableau suivant présente les tailles d’origine et compressées à l’aide de la bibliothèque de composants MudBlazorBlazor :

Fichier D’origine Compressé Réduction
MudBlazor.min.css 541 37,5 93,07%
MudBlazor.min.js 47.4 9.2 80,59%
Total 588,4 46,7 92,07%

L’optimisation se produit automatiquement lors de l’utilisation de MapStaticAssets. Lorsqu’une bibliothèque est ajoutée ou mise à jour, par exemple avec JavaScript ou CSS, les ressources sont optimisées dans le cadre de la build. L’optimisation est particulièrement bénéfique pour les environnements mobiles qui peuvent avoir une bande passante inférieure ou une connexion non fiable.

Pour plus d’informations sur les nouvelles fonctionnalités de remise de fichiers, consultez les ressources suivantes :

Activation de la compression dynamique sur le serveur par rapport à l’utilisation de MapStaticAssets

MapStaticAssets présente les avantages suivants par rapport à la compression dynamique sur le serveur :

  • Est plus simple, car il n’existe aucune configuration spécifique au serveur.
  • Est plus performant, car les ressources sont compressées au moment de la compilation.
  • Permet au développeur de passer du temps supplémentaire pendant le processus de génération pour s’assurer que les ressources sont de taille minimale.

Considérez le tableau suivant comparant la compression MudBlazor à la compression dynamique IIS et MapStaticAssets :

IIS gzip MapStaticAssets Réduction de MapStaticAssets
≅ 90 37,5 59 %

Blazor

Cette section décrit les nouvelles fonctionnalités pour Blazor.

Modèle de solution .NET MAUIBlazor Hybrid et d’application web

Un nouveau modèle de solution permet de faciliter la création d'applications clientes web .NET MAUI natives et Blazor qui partagent la même IU. Ce modèle montre comment créer des applications clientes qui maximisent la réutilisation de code et ciblent Android, iOS, Mac, Windows et le Web.

Les principales fonctionnalités de ce modèle comprennent :

  • La possibilité de choisir un mode de rendu interactif Blazor pour l’application web.
  • La création automatique des projets appropriés, y compris un Blazor Web App (rendu automatique interactif global) et une application .NET MAUIBlazor Hybrid.
  • Les projets créés utilisent une bibliothèque de classes Razor partagée (RCL) pour gérer les composants Razor de l’interface utilisateur.
  • L’exemple de code est inclus et montre comment utiliser l’injection de dépendances pour fournir différentes implémentations d’interface pour l’application Blazor Hybrid et l’Blazor Web App.

Pour commencer, installez le SDK .NET 9 et la charge de travail .NET MAUI qui contient le modèle :

dotnet workload install maui

Créez une solution à partir du modèle de projet dans un interpréteur de commandes à l’aide de la commande suivante :

dotnet new maui-blazor-web

Ce modèle est également disponible dans Visual Studio.

Remarque

Actuellement, une exception se produit si les modes de rendu Blazor sont définis au niveau par page ou par composant. Pour plus d’informations, consultez BlazorWebView a besoin d’un moyen d’activer le contournement de ResolveComponentForRenderMode (dotnet/aspnetcore #51235).

Pour plus d’informations, voir Construire une application .NET MAUIBlazor Hybrid avec un Blazor Web App.

Détecter l’emplacement de rendu, l’interactivité et le mode de rendu affecté au moment de l’exécution

Nous avons introduit une nouvelle API conçue pour simplifier le processus d’interrogation des états des composants au moment de l’exécution. Cette API fournit les fonctionnalités suivantes :

  • Déterminez l’emplacement d’exécution actuel du composant : cela peut être utile pour le débogage et l’optimisation des performances des composants.
  • Vérifier si le composant s’exécute dans un environnement interactif : cela peut être utile pour les composants qui présentent des comportements différents en fonction de l’interactivité de leur environnement.
  • Récupérer le mode de rendu affecté pour le composant : comprendre le mode de rendu peut aider à optimiser le processus de rendu et à améliorer les performances globales du composant.

Pour plus d’informations, consultez Modes de rendu ASP.NET Core Blazor.

Amélioration de l’expérience de reconnexion côté serveur :

Les améliorations suivantes ont été apportées à l’expérience de reconnexion côté serveur par défaut :

  • Lorsque l’utilisateur retourne à une application avec un circuit déconnecté, la reconnexion est tentée immédiatement plutôt que d’attendre pendant la durée du prochain intervalle de reconnexion. Cela améliore l’expérience utilisateur lors de la navigation vers une application dans un onglet de navigateur qui est passé en veille.

  • Lorsqu’une tentative de reconnexion atteint le serveur, mais que le serveur a déjà libéré le circuit, une actualisation de page se produit automatiquement. Cela empêche l’utilisateur d’avoir à actualiser manuellement la page si une reconnexion réussie est susceptible de se produire.

  • Le délai de reconnexion utilise une stratégie de backoff calculée. Par défaut, les premières tentatives de reconnexion se produisent rapidement sans intervalle avant nouvelle tentative avant l’introduction de délais calculés entre les tentatives. Vous pouvez personnaliser le comportement de l'intervalle de réessai en spécifiant une fonction pour calculer l’intervalle, comme l’exemple de backoff exponentiel suivant illustre :

    Blazor.start({
      circuit: {
        reconnectionOptions: {
          retryIntervalMilliseconds: (previousAttempts, maxRetries) => 
            previousAttempts >= maxRetries ? null : previousAttempts * 1000
        },
      },
    });
    
  • Le style de l’IU de reconnexion par défaut a été modernisé.

Pour plus d’informations, consultez Conseils relatifs à ASP.NET Core BlazorSignalR.

Sérialisation simplifiée de l’état d’authentification pour les Blazor Web App

Les nouvelles API facilitent l’ajout de l’authentification à une Blazor Web App existante. Lorsque vous créez une Blazor Web App avec l’authentification à l’aide de Comptes individuels et que vous activez l’interactivité basée sur WebAssembly, le projet inclut un AuthenticationStateProvider personnalisé dans les projets serveur et client.

Ces fournisseurs circulent l’état d’authentification de l’utilisateur vers le navigateur. L’authentification sur le serveur plutôt que sur le client permet à l’application d’accéder à l’état d’authentification pendant le prérendu et avant l’initialisation du runtime .NET WebAssembly.

Les implémentations personnalisées de AuthenticationStateProvider utilisent le service État de composant persistant (PersistentComponentState) pour sérialiser l’état d’authentification dans les commentaires HTML, et le lisent à partir de WebAssembly pour créer une instance AuthenticationState.

Cela fonctionne bien si vous avez commencé à partir du modèle de projet d’application Blazor Web App et que vous avez sélectionné l’option Comptes individuels. Cependant, cela représente beaucoup de code à implémenter ou à copier vous-même si vous essayez d’ajouter l’authentification à un projet existant. Il existe désormais des API qui font partie du modèle de projet d’Blazor Web App, qui peuvent être appelées dans les projets serveur et client pour ajouter cette fonctionnalité :

Par défaut, l’API sérialise uniquement le nom et les revendications de rôle côté serveur pour l’accès dans le navigateur. Une option peut être passée à AddAuthenticationStateSerialization pour inclure toutes les revendications.

Pour plus d’informations, consultez les sections suivantes concernant l'authentification et l’autorisation dans ASP.NET Core :

Ajouter des pages statiques de rendu côté serveur (SSR) à une page globalement interactive Blazor Web App

Avec la publication de .NET 9, il est désormais plus simple d’ajouter des pages SSR statiques aux applications qui adoptent l’interactivité globale.

Cette approche n’est utile que lorsque l’application a des pages spécifiques qui ne peuvent pas fonctionner avec le rendu interactif server ou WebAssembly. Par exemple, adoptez cette approche pour les pages qui dépendent de la lecture/l’écriture de cookies HTTP et peuvent uniquement fonctionner dans un cycle de requête/réponse au lieu d’un rendu interactif. Pour les pages qui fonctionnent avec le rendu interactif, vous ne devez pas les forcer à utiliser le rendu SSR statique, car il est moins efficace et moins réactif pour l’utilisateur final.

Marquez n’importe quelle Razor page de composant avec le nouvel [ExcludeFromInteractiveRouting] attribut affecté à la @attributeRazor directive :

@attribute [ExcludeFromInteractiveRouting]

L’application de l’attribut fait sortir la navigation vers la page du routage interactif. La navigation entrante est forcée d’effectuer un rechargement complet de la page au lieu de résoudre la page via le routage interactif. Le rechargement complet de la page force le composant racine de niveau supérieur, généralement le composant App (App.razor), à effectuer un nouveau rendu à partir du serveur, ce qui permet à l’application de basculer vers un autre mode de rendu de niveau supérieur.

La RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting méthode d’extension permet au composant de détecter si l’attribut [ExcludeFromInteractiveRouting] est appliqué à la page active.

Dans le composant App, utilisez le modèle dans l’exemple suivant :

  • Les pages qui ne sont pas annotées avec l'attribut [ExcludeFromInteractiveRouting] adoptent par défaut le mode de rendu InteractiveServer avec interactivité globale. Vous pouvez remplacer InteractiveServer par InteractiveWebAssembly ou InteractiveAuto pour spécifier un autre mode de rendu global par défaut.
  • Les pages annotées avec l’attribut [ExcludeFromInteractiveRouting] adoptent le SSR statique (PageRenderModeest affecténull).
<!DOCTYPE html>
<html>
<head>
    ...
    <HeadOutlet @rendermode="@PageRenderMode" />
</head>
<body>
    <Routes @rendermode="@PageRenderMode" />
    ...
</body>
</html>

@code {
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    private IComponentRenderMode? PageRenderMode
        => HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null;
}

Une alternative à l’utilisation de la méthode d’extension RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting consiste à lire manuellement les métadonnées de point de terminaison à l’aide de HttpContext.GetEndpoint()?.Metadata.

Cette fonctionnalité est couverte par la documentation de référence dans Modes de rendu Blazor ASP.NET Core.

Injection de constructeur

Les composants Razor prennent en charge l’injection de constructeur.

Dans l’exemple suivant, la classe partielle (code-behind) injecte le service NavigationManager avec un constructeur principal :

public partial class ConstructorInjection(NavigationManager navigation)
{
    private void HandleClick()
    {
        navigation.NavigateTo("/counter");
    }
}

Pour plus d’informations, consultez Injection de dépendances Blazor ASP.NET Core.

Compression WebSocket pour les composants de serveur interactif

Par défaut, les composants Interactive Server activent la compression pour les connexions WebSocket et définissent une frame-ancestors directive CSP (Content Security Policy) définie 'self' sur (guide de référence du fournisseur de services cloud mdN), ce qui autorise uniquement l’incorporation de l’application dans une <iframe> origine à partir de laquelle l’application est servie quand la compression est activée ou lorsqu’une configuration pour le contexte WebSocket est fournie.

La compression peut être désactivée en définissant ConfigureWebSocketOptions sur null, ce qui réduit la vulnérabilité de l’application à attaquer, mais peut entraîner une réduction des performances :

.AddInteractiveServerRenderMode(o => o.ConfigureWebSocketOptions = null)

Configurez un CSP frame-ancestors plus strict avec une valeur de 'none' (guillemets simples requis), qui autorise la compression WebSocket mais empêche les navigateurs d’incorporer l’application dans n’importe quel <iframe>:

.AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = "'none'")

Pour plus d’informations, consultez les ressources suivantes :

Gérer les événements de composition du clavier dans Blazor

La nouvelle propriété KeyboardEventArgs.IsComposing indique si l’événement de clavier fait partie d’une session de composition. Le suivi de l’état de composition des événements de clavier est essentiel pour gérer les méthodes d’entrée de caractères internationaux.

Ajout du paramètre OverscanCount pour QuickGrid

Le composant QuickGrid expose désormais une propriété OverscanCount qui spécifie le nombre de lignes supplémentaires rendues avant et après la région visible lorsque la virtualisation est activée.

La valeur par défaut de OverscanCount est 3. L’exemple suivant augmente la valeur OverscanCount à 4 :

<QuickGrid ItemsProvider="itemsProvider" Virtualize="true" OverscanCount="4">
    ...
</QuickGrid>

Le composant InputNumber prend en charge l’attribut type="range".

Le composant InputNumber<TValue> prend désormais en charge l'attribut type="range", créant une entrée de gamme qui supporte la liaison de modèle et la validation de formulaire, généralement rendue par un curseur ou un contrôle rotatif plutôt qu'une zone de texte.

<EditForm Model="Model" OnSubmit="Submit" FormName="EngineForm">
    <div>
        <label>
            Nacelle Count (2-6): 
            <InputNumber @bind-Value="Model!.NacelleCount" max="6" min="2" 
                step="1" type="range" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private EngineSpecifications? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() {}

    public class EngineSpecifications
    {
        [Required, Range(minimum: 2, maximum: 6)]
        public int NacelleCount { get; set; }
    }
}

Nouveaux événements de navigation améliorés

Déclenchez des fonctions de rappel JavaScript soit avant, soit après une navigation améliorée avec de nouveaux gestionnaires d'événements :

  • blazor.addEventListener("enhancednavigationstart", {CALLBACK})
  • blazor.addEventListener("enhancednavigationend", {CALLBACK})

Pour plus d’informations, veuillez consulter ASP.NET Core Blazor JavaScript avec rendu côté serveur statique (static SSR).

Diffusion en continu des demandes côté client

Le rendu interactif de WebAssembly dans Blazor prend désormais en charge le streaming des requêtes client à l’aide de l’option request.SetBrowserRequestStreamingEnabled(true) sur HttpRequestMessage.

Pour plus d’informations, consultez les ressources suivantes :

SignalR

Cette section décrit les nouvelles fonctionnalités pour SignalR.

Prise en charge des types polymorphes dans les Hubs SignalR

Les méthodes Hub peuvent désormais accepter une classe de base au lieu de la classe dérivée pour prendre en charge les scénarios polymorphes. Le type de base doit être annoté pour autoriser le polymorphisme.

public class MyHub : Hub
{
    public void Method(JsonPerson person)
    {
        if (person is JsonPersonExtended)
        {
        }
        else if (person is JsonPersonExtended2)
        {
        }
        else
        {
        }
    }
}

[JsonPolymorphic]
[JsonDerivedType(typeof(JsonPersonExtended), nameof(JsonPersonExtended))]
[JsonDerivedType(typeof(JsonPersonExtended2), nameof(JsonPersonExtended2))]
private class JsonPerson
{
    public string Name { get; set; }
    public Person Child { get; set; }
    public Person Parent { get; set; }
}

private class JsonPersonExtended : JsonPerson
{
    public int Age { get; set; }
}

private class JsonPersonExtended2 : JsonPerson
{
    public string Location { get; set; }
}

Activités Améliorées pour SignalR

SignalR dispose désormais d’une ActivitySource pour le serveur hub et le client.

ActivitySource de serveur SignalR .NET

L’ActivitySource SignalR nommée Microsoft.AspNetCore.SignalR.Server émet des événements pour les appels de méthode hub :

  • Chaque méthode est sa propre activité, de sorte que tout ce qui émet une activité pendant l’appel de méthode hub se trouve sous l’activité de méthode hub.
  • Les activités des méthodes du hub n’ont pas de parent. Cela signifie qu’ils ne sont pas bundlés sous la connexion SignalR de longue durée.

L’exemple suivant utilise le tableau de bord Aspire et les packages OpenTelemetry :

<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />

Ajoutez le code de démarrage suivant au fichier Program.cs :

using OpenTelemetry.Trace;
using SignalRChat.Hubs;

// Set OTEL_EXPORTER_OTLP_ENDPOINT environment variable depending on where your OTEL endpoint is.
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        if (builder.Environment.IsDevelopment())
        {
            // View all traces only in development environment.
            tracing.SetSampler(new AlwaysOnSampler());
        }

        tracing.AddAspNetCoreInstrumentation();
        tracing.AddSource("Microsoft.AspNetCore.SignalR.Server");
    });

builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());

var app = builder.Build();

Voici un exemple de sortie du tableau de bord Aspire :

Liste d’activités pour les événements d’appel de méthode hub SignalR

ActivitySource de client SignalR .NET

L’ActivitySource SignalR nommée Microsoft.AspNetCore.SignalR.Client émet des événements pour un client SignalR :

  • Le client .NET SignalR dispose d’un ActivitySource nommé Microsoft.AspNetCore.SignalR.Client. Les appels du hub créent désormais une span client. Notez que d’autres clients SignalR, tels que le client JavaScript, ne prennent pas en charge le traçage. Cette fonctionnalité sera ajoutée à davantage de clients dans les prochaines versions.
  • Les appels hub sur le client et le serveur prennent en charge la propagation de contexte. La propagation du contexte de traçage permet un véritable traçage distribué. Il est désormais possible de voir les invocations circuler du client vers le serveur et vice versa.

Voici à quoi ressemblent ces nouvelles activités dans le tableau de bord Aspire :

SignalR suivi distribué dans le Aspire tableau de bord

SignalR prend en charge l’élagage et l’AOT natif

Pour poursuivre le développement de l’AOT natif commencé dans .NET 8, nous avons activé la prise en charge du découpage et de la compilation anticipée (AOT) native pour les scénarios client et serveur SignalR. Vous pouvez désormais tirer parti des avantages en matière de performances de l’utilisation de l’AOT natif dans les applications qui utilisent SignalR pour les communications web en temps réel.

Mise en route

Installez le kit SDK .NET 9 le plus récent.

Créez une solution à partir du modèle webapiaot dans un interpréteur de commandes à l’aide de la commande suivante :

dotnet new webapiaot -o SignalRChatAOTExample

Remplacez le contenu du fichier Program.cs par le code SignalR suivant :

using Microsoft.AspNetCore.SignalR;
using System.Text.Json.Serialization;

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.AddSignalR();
builder.Services.Configure<JsonHubProtocolOptions>(o =>
{
    o.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

var app = builder.Build();

app.MapHub<ChatHub>("/chatHub");
app.MapGet("/", () => Results.Content("""
<!DOCTYPE html>
<html>
<head>
    <title>SignalR Chat</title>
</head>
<body>
    <input id="userInput" placeholder="Enter your name" />
    <input id="messageInput" placeholder="Type a message" />
    <button onclick="sendMessage()">Send</button>
    <ul id="messages"></ul>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.7/signalr.min.js"></script>
    <script>
        const connection = new signalR.HubConnectionBuilder()
            .withUrl("/chatHub")
            .build();

        connection.on("ReceiveMessage", (user, message) => {
            const li = document.createElement("li");
            li.textContent = `${user}: ${message}`;
            document.getElementById("messages").appendChild(li);
        });

        async function sendMessage() {
            const user = document.getElementById("userInput").value;
            const message = document.getElementById("messageInput").value;
            await connection.invoke("SendMessage", user, message);
        }

        connection.start().catch(err => console.error(err));
    </script>
</body>
</html>
""", "text/html"));

app.Run();

[JsonSerializable(typeof(string))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

L’exemple précédent produit un exécutable Windows natif de 10 Mo et un exécutable Linux de 10,9 Mo.

Limites

  • Seul le protocole JSON est actuellement pris en charge :
    • Comme indiqué dans le code précédent, les applications qui utilisent la sérialisation JSON et l’AOT natif doivent utiliser le générateur de source System.Text.Json.
    • Cela suit la même approche que les API minimales.
  • Sur le serveur SignalR, les paramètres de la méthode de hub de type IAsyncEnumerable<T> et ChannelReader<T>T est une propriété ValueType (c’est-à-dire struct) ne sont pas pris en charge. L’utilisation de ces types entraîne une exception d’exécution au démarrage en développement et dans l’application publiée. Pour plus d’informations, consultez SignalR : Utilisation de IAsyncEnumerable<T> et ChannelReader<T> avec des propriétés ValueType dans l’AOT natif (dotnet/aspnetcore #56179).
  • Les hubs fortement typés ne sont pas pris en charge par Native AOT (PublishAot). L’utilisation de hubs fortement typés avec Native AOT entraînera des avertissements lors de la construction et de la publication, ainsi qu’une exception d’exécution. L’utilisation de hubs fortement typés avec élagage (PublishedTrimmed) est prise en charge.
  • Seuls Task, Task<T>, ValueTask ou ValueTask<T> sont pris en charge pour les types de retour asynchrones.

API minimales

Cette section décrit les nouvelles fonctionnalités des API minimales.

Ajout de InternalServerError et InternalServerError<TValue> à TypedResults

La TypedResults classe est un moyen efficace pour retourner des réponses fortement typées basées sur des codes d'état HTTP d'une API minimale. TypedResults inclut désormais des méthodes et des types d’usine pour renvoyer des réponses « 500 Internal Server Error » à partir des points de terminaison. Voici un exemple qui retourne une réponse 500 :

var app = WebApplication.Create();

app.MapGet("/", () => TypedResults.InternalServerError("Something went wrong!"));

app.Run();

Appeler ProducesProblem et ProducesValidationProblem sur les groupes d’itinéraires

Les méthodes d'extension ProducesProblem et ProducesValidationProblem ont été mises à jour pour prendre en charge leur utilisation sur des groupes de routes. Ces méthodes indiquent que tous les points de terminaison d’un groupe de route peuvent retourner des réponses ProblemDetails ou ValidationProblemDetails pour les fins des métadonnées OpenAPI.

var app = WebApplication.Create();

var todos = app.MapGroup("/todos")
    .ProducesProblem();

todos.MapGet("/", () => new Todo(1, "Create sample app", false));
todos.MapPost("/", (Todo todo) => Results.Ok(todo));

app.Run();

record Todo(int Id, string Title, boolean IsCompleted);

Les types de résultat Problem et ValidationProblem permettent la construction avec des valeurs IEnumerable<KeyValuePair<string, object?>>.

Avant .NET 9, la construction des types de résultats Problem et ValidationProblem dans les API Minimales exigeait que les propriétés errors et extensions soient initialisées avec une implémentation de IDictionary<string, object?>. Dans cette version, ces API de construction prennent en charge les surcharges qui consomment IEnumerable<KeyValuePair<string, object?>>.

var app = WebApplication.Create();

app.MapGet("/", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions",
                                                       extensions: extensions);
});

Merci à l’utilisateur GitHub joegoldman2 pour cette contribution !

OpenAPI

Cette section décrit les nouvelles fonctionnalités pour OpenAPI.

Prise en charge intégrée de la génération de documents OpenAPI

La spécification OpenAPI est une norme permettant de décrire les API HTTP. La norme permet aux développeurs de définir la forme des API qui peuvent être connectées aux générateurs clients, aux générateurs de serveurs, aux outils de test, à la documentation, etc. Dans .NET 9, ASP.NET Core fournit une prise en charge intégrée de la génération de documents OpenAPI représentant des API minimales ou basées sur un contrôleur via le package Microsoft.AspNetCore.OpenApi .

Le code suivant, mis en évidence, appelle :

  • AddOpenApi pour inscrire les dépendances requises dans le conteneur DI de l’application.
  • MapOpenApi pour inscrire les points de terminaison OpenAPI requis dans les itinéraires de l’application.
var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/hello/{name}", (string name) => $"Hello {name}"!);

app.Run();

Installez le package Microsoft.AspNetCore.OpenApi dans le projet à l’aide de la commande suivante :

dotnet add package Microsoft.AspNetCore.OpenApi

Exécutez l’application et accédez à openapi/v1.json pour afficher le document OpenAPI généré :

Document OpenAPI

Vous pouvez également créer les documents OpenAPI au moment de la génération en ajoutant le package Microsoft.Extensions.ApiDescription.Server :

dotnet add package Microsoft.Extensions.ApiDescription.Server

Pour modifier l’emplacement des documents OpenAPI émis, définissez le chemin d’accès cible dans la propriété OpenApiDocumentsDirectory dans le fichier projet de l’application :

<PropertyGroup>
  <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
</PropertyGroup>

Exécutez dotnet build et inspectez le fichier JSON généré dans le répertoire du projet.

Génération de documents OpenAPI au moment de la génération

La génération de documents OpenAPI intégrée à ASP.NET Core prend en charge diverses personnalisations et options. Il fournit des transformateurs de document, d’opération et de schéma et a la possibilité de gérer plusieurs documents OpenAPI pour la même application.

Pour en savoir plus sur les nouvelles fonctionnalités de document OpenAPI d’ASP.NET Core, consultez la nouvelle documentation Microsoft.AspNetCore.OpenApi.

Microsoft.AspNetCore.OpenApi prend en charge le découpage et l’AOT natif

OpenAPI dans ASP.NET Core prend en charge le trimming et l’AOT natif. Les étapes suivantes décrivent comment créer et publier une application OpenAPI avec découpage et AOT native :

Créez un projet d’API web ASP.NET Core (AOT natif).

dotnet new webapiaot

Ajoutez le package Microsoft.AspNetCore.OpenAPI.

dotnet add package Microsoft.AspNetCore.OpenApi

Mettre à jour Program.cs pour activer la génération de documents OpenAPI.

+ builder.Services.AddOpenApi();

var app = builder.Build();

+ app.MapOpenApi();

Publiez l’application.

dotnet publish

Authentification et autorisation

Cette section décrit les nouvelles fonctionnalités pour l’authentification et l’autorisation.

OpenIdConnectHandler ajoute la prise en charge des demandes d’autorisation push (PAR)

Nous aimerions remercier Joe DeCock de Duende Software pour l’ajout de demandes d’autorisation push (PAR) à OpenIdConnectHandler dans ASP.NET Core. Joe a décrit l’arrière-plan et la motivation de l’activation de PAR dans sa proposition d’API comme suit :

Les demandes d’autorisation push (PAR) sont une norme OAuth relativement nouvelle qui améliore la sécurité des flux OAuth et OIDC en déplaçant les paramètres d’autorisation du canal frontal vers le canal arrière. Il s’agit de déplacer les paramètres d’autorisation des URL de redirection dans le navigateur vers les appels http directs de machine à machine sur le back-end.

Cela empêche un cyberattaquant dans le navigateur de pouvoir :

  • Afficher les paramètres d’autorisation, ce qui pourrait fuiter des informations d’identification personnelle
  • Altérer ces paramètres. Le cyberattaquant pourrait par exemple modifier l’étendue de l’accès demandé.

L’envoi des paramètres d’autorisation permet également des URL de requête courtes. Les paramètres d’autorisation peuvent être très longs lors de l’utilisation de fonctionnalités OAuth et OIDC plus complexes, telles que les demandes d’autorisation enrichies. Les URL qui sont longues provoquent des problèmes dans de nombreux navigateurs et infrastructures réseau.

L’utilisation de la PAR est encouragée par le groupe de travail FAPI au sein de l’OpenID Foundation. Par exemple, le profil de sécurité FAPI2.0 nécessite l’utilisation de la PAR. Ce profil de sécurité est utilisé par de nombreux groupes travaillant sur des systèmes bancaires ouverts (principalement en Europe), dans le secteur de la santé et dans d’autres secteurs ayant des exigences de sécurité élevées.

PAR est pris en charge par un certain nombre de fournisseurs d’identité, notamment

Pour .NET 9, nous avons décidé d’activer PAR par défaut si le document de découverte du fournisseur d’identité publie la prise en charge de PAR, car il doit fournir une sécurité renforcée pour les fournisseurs qui le prennent en charge. Le document de découverte du fournisseur d’identité se trouve généralement ici : .well-known/openid-configuration. Si cela pose des problèmes, vous pouvez désactiver PAR via OpenIdConnectOptions.PushedAuthorizationBehavior comme suit :

builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect("oidc", oidcOptions =>
    {
        // Other provider-specific configuration goes here.

        // The default value is PushedAuthorizationBehavior.UseIfAvailable.

        // 'OpenIdConnectOptions' does not contain a definition for 'PushedAuthorizationBehavior'
        // and no accessible extension method 'PushedAuthorizationBehavior' accepting a first argument
        // of type 'OpenIdConnectOptions' could be found
        oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Disable;
    });

Pour garantir que l’authentification ne réussisse que si PAR est utilisé, utilisez PushedAuthorizationBehavior.Require à la place. Cette modification introduit également un nouvel événement OnPushAuthorization dans OpenIdConnectEvents, qui peut être utilisé pour personnaliser la demande d’autorisation push ou la gérer manuellement. Pour plus d’informations, consultez la proposition d’API.

Personnalisation des paramètres OAuth et OIDC

Les gestionnaires d’authentification OIDC et OAuth dispose désormais d’une option AdditionalAuthorizationParameters pour faciliter la personnalisation des paramètres de messages d’autorisation qui sont généralement inclus dans le cadre de la chaîne de requête de redirection. Dans .NET 8 ou version antérieure, cela nécessite un rappel personnalisé OnRedirectToIdentityProvider ou une méthode substituée BuildChallengeUrl dans un gestionnaire personnalisé. Voici un exemple de code .NET 8 :

builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
    options.Events.OnRedirectToIdentityProvider = context =>
    {
        context.ProtocolMessage.SetParameter("prompt", "login");
        context.ProtocolMessage.SetParameter("audience", "https://api.example.com");
        return Task.CompletedTask;
    };
});

L’exemple précédent peut maintenant être simplifié par le code suivant :

builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
    options.AdditionalAuthorizationParameters.Add("prompt", "login");
    options.AdditionalAuthorizationParameters.Add("audience", "https://api.example.com");
});

Configurer les indicateurs étendus d’authentification de HTTP.sys

Vous pouvez maintenant configurer les indicateurs HTTP.sys HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING et HTTP_AUTH_EX_FLAG_CAPTURE_CREDENTIAL en utilisant les nouvelles propriétés EnableKerberosCredentialCaching et CaptureCredentials sur le serveur HTTP.sys AuthenticationManager pour optimiser la méthode de gestion de l’Authentification Windows. Par exemple :

webBuilder.UseHttpSys(options =>
{
    options.Authentication.Schemes = AuthenticationSchemes.Negotiate;
    options.Authentication.EnableKerberosCredentialCaching = true;
    options.Authentication.CaptureCredentials = true;
});

Divers

Les sections suivantes décrivent diverses nouvelles fonctionnalités.

Nouvelle bibliothèque HybridCache

Importante

HybridCache est actuellement en préversion, mais sera entièrement publié après .NET 9 dans une prochaine version mineure des extensions .NET.

L’API HybridCache permet de combler certaines lacunes dans les API IDistributedCache et IMemoryCache. Elle ajoute également de nouvelles fonctionnalités, telles que :

  • Protection « Stampede » pour empêcher les extractions parallèles du même travail.
  • Sérialisation configurable.

HybridCache est conçu pour être un remplacement direct de l'utilisation existante des IDistributedCache et IMemoryCache, et il fournit une API simple pour l'intégration de nouveau code de mise en cache. Elle fournit une API unifiée pour la mise en cache à l'intérieur d'un processus et à l'extérieur d'un processus.

Pour voir comment l’API HybridCache est simplifiée, comparez-la au code qui utilise IDistributedCache. Voici un exemple d’utilisation de IDistributedCache :

public class SomeService(IDistributedCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        var key = $"someinfo:{name}:{id}"; // Unique key for this combination.
        var bytes = await cache.GetAsync(key, token); // Try to get from cache.
        SomeInformation info;
        if (bytes is null)
        {
            // Cache miss; get the data from the real source.
            info = await SomeExpensiveOperationAsync(name, id, token);

            // Serialize and cache it.
            bytes = SomeSerializer.Serialize(info);
            await cache.SetAsync(key, bytes, token);
        }
        else
        {
            // Cache hit; deserialize it.
            info = SomeSerializer.Deserialize<SomeInformation>(bytes);
        }
        return info;
    }

    // This is the work we're trying to cache.
    private async Task<SomeInformation> SomeExpensiveOperationAsync(string name, int id,
        CancellationToken token = default)
    { /* ... */ }
}

Cela représente beaucoup d’opérations à réaliser correctement à chaque fois, y compris la sérialisation. Et dans le scénario d’échec d’accès au cache, vous risquez de vous retrouver avec plusieurs threads simultanés, qui reçoivent tous un échec d’accès au cache, qui récupèrent tous les données sous-jacentes, qui les sérialisent tous et qui envoient tous ces données au cache.

Pour simplifier et améliorer ce code avec HybridCache, nous devons d’abord ajouter la nouvelle bibliothèque Microsoft.Extensions.Caching.Hybrid :

<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0" />

Inscrivez le service HybridCache, comme vous le feriez pour inscrire une implémentation IDistributedCache :

builder.Services.AddHybridCache(); // Not shown: optional configuration API.

À présent, la plupart des problèmes de mise en cache peuvent être déchargés vers HybridCache :

public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // Unique key for this combination.
            async cancel => await SomeExpensiveOperationAsync(name, id, cancel),
            token: token
        );
    }
}

Nous fournissons une implémentation concrète de la classe abstraite HybridCache via l’injection de dépendances, mais il est prévu que les développeurs puissent fournir des implémentations personnalisées de l’API. L’implémentation de HybridCache traite de tout ce qui concerne la mise en cache, y compris la gestion simultanée des opérations. Le jeton cancel représente ici l’annulation combinée de tous les appelants simultanés, pas seulement l’annulation de l’appelant que nous pouvons voir (autrement dit, token).

Les scénarios à débit élevé peuvent être optimisés davantage à l’aide du modèle TState pour éviter une surcharge des variables capturées et des rappels par instance :

public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync(string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // unique key for this combination
            (name, id), // all of the state we need for the final call, if needed
            static async (state, token) =>
                await SomeExpensiveOperationAsync(state.name, state.id, token),
            token: token
        );
    }
}

HybridCache utilise l’implémentation IDistributedCache configurée, si disponible, pour la mise en cache hors processus secondaire, par exemple en utilisant Redis. Mais même en l’absence de IDistributedCache, le service HybridCache continuera à assurer la mise en cache en cours de processus et la protection contre la « ruée ».

Remarque sur la réutilisation des objets

Dans le code existant classique qui utilise IDistributedCache, chaque récupération d’un objet à partir du cache entraîne la désérialisation. Ce comportement signifie que chaque appelant simultané obtient une instance distincte de l’objet, qui ne peut pas interagir avec d’autres instances. Il en résulte une sécurité des threads, car il n’existe aucun risque de modifications simultanées sur la même instance d’objet.

Étant donné que nombre d’utilisations de HybridCache seront adaptées à partir du code IDistributedCache existant, HybridCache conserve ce comportement par défaut pour éviter d’introduire des bogues d’accès concurrentiel. Toutefois, un cas d’utilisation donné est intrinsèquement thread-safe :

  • Si les types mis en cache sont immuables.
  • Si le code ne les modifie pas.

Dans ce cas, informez-vous HybridCache qu’il est sûr de réutiliser des instances en apportant les deux modifications suivantes :

  • Marquer le type comme sealed. Le mot clé sealed en C# signifie que la classe ne peut pas être héritée.
  • Appliquant l’attribut [ImmutableObject(true)] à celui-ci. L’attribut [ImmutableObject(true)] indique que l’état de l’objet ne peut pas être modifié après sa création.

En réutilisant des instances, HybridCache peut réduire la charge de traitement des allocations de processeur et d’objets associées à la désérialisation par appel. Cela peut entraîner des améliorations des performances dans les scénarios où les objets mis en cache sont volumineux ou consultés fréquemment.

Autres fonctionnalités HybridCache

Comme IDistributedCache, HybridCache prend en charge la suppression par clé avec une méthode RemoveKeyAsync.

HybridCache fournit également des API facultatives pour les implémentations de IDistributedCache, afin d’éviter les allocations byte[]. Cette fonctionnalité est implémentée par les versions préliminaires des packages Microsoft.Extensions.Caching.StackExchangeRedis, Microsoft.Extensions.Caching.SqlServer et Microsoft.Extensions.Caching.Postgres.

La sérialisation est configurée dans le cadre de l’inscription du service, permettant d'utiliser des sérialiseurs propres au type et généralisés via les méthodes WithSerializer et .WithSerializerFactory, enchaînés à partir de l'appel AddHybridCache. Par défaut, la bibliothèque gère string et byte[] en interne, et utilise System.Text.Json pour tout le reste, mais vous pouvez utiliser protobuf, xml ou autre.

HybridCache prend en charge les runtimes .NET plus anciens, jusqu’à .NET Framework 4.7.2 et .NET Standard 2.0.

Pour plus d’informations sur HybridCache, consultez Bibliothèque HybridCache dans ASP.NET Core

Améliorations apportées à la page d’exceptions du développeur

La page d’exceptions du développeur ASP.NET Core s’affiche lorsqu’une application lève une exception non gérée pendant le développement. La page d’exceptions du développeur fournit des informations détaillées sur l’exception et la requête.

La préversion 3 a ajouté des métadonnées de point de terminaison à la page d’exception du développeur. ASP.NET Core utilise les métadonnées du point de terminaison pour contrôler le comportement du point de terminaison, comme le routage, la mise en cache des réponses, la limitation du débit, la génération OpenAPI, etc. L’image suivante montre les nouvelles informations de métadonnées de la section Routing sur la page d’exceptions du développeur :

Les nouvelles informations de métadonnées sur la page d’exception du développeur

Lors du test de la page d'exception du développeur, des petites améliorations de confort ont été identifiées. Elles ont été livrées dans la Preview 4 :

  • Meilleur habillage du texte. Les cookies, les valeurs de chaîne de requête et les noms de méthode longs n’ajoutent plus de barres de défilement de navigateur horizontales.
  • Texte de plus grande taille que l’on trouve dans les designs modernes.
  • Tailles de table plus homogènes.

L’image animée suivante montre la nouvelle page d’exceptions du développeur :

La nouvelle page d’exceptions du développeur

Amélioration du débogage des dictionnaires

L’affichage de débogage des dictionnaires et autres collections clé-valeur a été amélioré. La clé est affichée dans la colonne clé du débogueur au lieu d’être concaténée avec la valeur. Les images suivantes montrent l’ancien et le nouvel affichage d’un dictionnaire dans le débogueur.

Avant :

L’expérience précédente du débogueur

Après :

La nouvelle expérience de débogueur

ASP.NET Core a de nombreuses collections clé-valeur. Cette expérience de débogage améliorée s’applique à :

  • En-têtes HTTP
  • Chaînes de requête
  • Formulaires
  • Cookies
  • Afficher les données
  • Données de routage
  • Fonctionnalités

Correctif pour les erreurs 503 pendant le recyclage d'applications dans IIS

Par défaut, il existe maintenant un délai de 1 seconde entre le moment où IIS est averti d’un recyclage ou d’un arrêt et le moment où ANCM indique au serveur managé de démarrer l’arrêt. Le délai est configurable via la variable d’environnement ANCM_shutdownDelay ou en définissant le paramètre du gestionnaire shutdownDelay. Les deux valeurs sont en millisecondes. Le délai est principalement destiné à réduire la probabilité d’une course où :

  • IIS n’a pas démarré la mise en file d’attente des requêtes pour accéder à la nouvelle application.
  • ANCM commence à rejeter les nouvelles requêtes qui entrent dans l’ancienne application.

Les machines plus lentes ou recourant davantage au processeur peuvent vouloir ajuster cette valeur pour réduire la probabilité d’erreurs 503.

Exemple de paramètre shutdownDelay :

<aspNetCore processPath="dotnet" arguments="myapp.dll" stdoutLogEnabled="false" stdoutLogFile=".logsstdout">
  <handlerSettings>
    <!-- Milliseconds to delay shutdown by.
    this doesn't mean incoming requests will be delayed by this amount,
    but the old app instance will start shutting down after this timeout occurs -->
    <handlerSetting name="shutdownDelay" value="5000" />
  </handlerSettings>
</aspNetCore>

Le correctif se trouve dans le module ANCM installé à l’échelle mondiale qui provient du pack d’hébergement.

ASP0026 : Analyseur à avertir lorsque [Authorize] est remplacé par [AllowAnonymous] de « plus loin »

Il semble logique qu’un attribut [Authorize] placé « plus près » d’une action MVC qu’un attribut [AllowAnonymous] remplacerait l’attribut [AllowAnonymous] et forcerait l’autorisation. Pourtant, ce n’est pas nécessairement le cas. En fait, c’est l’ordre relatif des attributs qui compte.

Remarque

L’attribut [AllowAnonymous] ne désactive pas entièrement l’authentification. Lorsque les informations d’identification sont envoyées à un point de terminaison avec [AllowAnonymous], le point de terminaison authentifie toujours ces informations d’identification et établit l’identité de l’utilisateur. L’attribut signifie uniquement que l’authentification [AllowAnonymous]n’est pas requise : le point de terminaison s’exécute en tant qu’anonyme uniquement lorsqu’aucune information d’identification n’est fournie. Ce comportement peut être utile pour les points de terminaison qui doivent fonctionner à la fois pour les utilisateurs authentifiés et anonymes.

Le code suivant montre des exemples où un attribut [Authorize] plus près est remplacé par un attribut [AllowAnonymous] plus loin.

[AllowAnonymous]
public class MyController
{
    [Authorize] // Overridden by the [AllowAnonymous] attribute on the class
    public IActionResult Private() => null;
}
[AllowAnonymous]
public class MyControllerAnon : ControllerBase
{
}

[Authorize] // Overridden by the [AllowAnonymous] attribute on MyControllerAnon
public class MyControllerInherited : MyControllerAnon
{
}

public class MyControllerInherited2 : MyControllerAnon
{
    [Authorize] // Overridden by the [AllowAnonymous] attribute on MyControllerAnon
    public IActionResult Private() => null;
}
[AllowAnonymous]
[Authorize] // Overridden by the preceding [AllowAnonymous]
public class MyControllerMultiple : ControllerBase
{
}

Dans .NET 9 Preview 6, nous avons introduit un analyseur qui met en évidence des instances comme celles où un attribut [Authorize] plus près est remplacé par un attribut [AllowAnonymous] qui se trouve plus loin d’une action MVC. L’avertissement pointe vers l’attribut [Authorize] écrasé avec le message suivant :

ASP0026 [Authorize] overridden by [AllowAnonymous] from farther away

L’action appropriée à entreprendre si vous voyez cet avertissement dépend de l’intention derrière les attributs. L’attribut [AllowAnonymous] plus loin doit être supprimé s’il expose involontairement le point de terminaison à des utilisateurs anonymes. Si l’attribut [AllowAnonymous] était destiné à remplacer un attribut [Authorize] plus près, vous pouvez répéter l’attribut [AllowAnonymous] après l’attribut [Authorize] pour clarifier l’intention.

[AllowAnonymous]
public class MyController
{
    // This produces no warning because the second, "closer" [AllowAnonymous]
    // clarifies that [Authorize] is intentionally overridden.
    // Specifying AuthenticationSchemes can be useful for endpoints that
    // allow but don't require authenticated users. When credentials are sent,
    // they will be authenticated; when no credentials are sent, the endpoint
    // allows anonymous access.
    [Authorize(AuthenticationSchemes = "Cookies")]
    [AllowAnonymous]
    public IActionResult Privacy() => null;
}

Amélioration des métriques de connexion Kestrel

Nous avons apporté une amélioration significative aux métriques de connexion Kestrel en incluant des métadonnées sur la raison de l’échec d’une connexion. La métrique kestrel.connection.duration inclut désormais la raison de la fermeture de la connexion dans l’attribut error.type.

Voici un petit échantillon des valeurs error.type :

  • tls_handshake_failed - La connexion nécessite TLS et le handshake TLS a échoué.
  • connection_reset - La connexion a été fermée de manière inattendue par le client pendant que des requêtes étaient en cours.
  • request_headers_timeout - Kestrel a fermé la connexion, car elle n’a pas reçu les en-têtes de requête dans le délai imparti.
  • max_request_body_size_exceeded - Kestrel a fermé la connexion, car les données chargées dépassaient la taille maximale.

Auparavant, le diagnostic des problèmes de connexion Kestrel exigeait qu’un serveur enregistre une journalisation détaillée et de bas niveau. Cependant, les journaux peuvent être coûteux à générer et à stocker, et il peut être difficile de trouver la bonne information parmi le bruit.

Les métriques sont une alternative beaucoup moins coûteuse qui peut rester activée dans un environnement de production avec un impact minimal. Les métriques collectées peuvent générer des tableaux de bord et des alertes. Une fois qu’un problème est identifié à un niveau élevé à l’aide des métriques, un examen approfondi à l’aide de la journalisation et d’autres outils peut commencer.

Les métriques de connexion améliorées devraient être utiles dans de nombreux scénarios :

  • Examen des problèmes de performances causés par des durées de vie de connexion courtes
  • Observation des attaques externes en cours sur Kestrel qui ont un impact sur les performances et la stabilité
  • Enregistrement des tentatives d’attaques externes sur Kestrel que le renforcement de la sécurité intégré de Kestrel a empêché.

Pour plus d’informations, consultez Métriques ASP.NET Core.

Personnaliser les points de terminaison du canal nommé Kestrel

La prise en charge des canaux nommés par Kestrel a été améliorée grâce à des options de personnalisation avancées. La nouvelle méthode CreateNamedPipeServerStream sur les options de canal nommé permet de personnaliser les canaux par point de terminaison.

Par exemple, elle est particulièrement utile lorsqu’une application Kestrel nécessite deux points de terminaison de canal avec une sécurité d’accès différente. L’option CreateNamedPipeServerStream peut être utilisée pour créer des canaux avec des paramètres de sécurité personnalisés, en fonction du nom du canal.

var builder = WebApplication.CreateBuilder();

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenNamedPipe("pipe1");
    options.ListenNamedPipe("pipe2");
});

builder.WebHost.UseNamedPipes(options =>
{
    options.CreateNamedPipeServerStream = (context) =>
    {
        var pipeSecurity = CreatePipeSecurity(context.NamedPipeEndpoint.PipeName);

        return NamedPipeServerStreamAcl.Create(context.NamedPipeEndPoint.PipeName, PipeDirection.InOut,
            NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte,
            context.PipeOptions, inBufferSize: 0, outBufferSize: 0, pipeSecurity);
    };
});

Option ExceptionHandlerMiddleware permettant de choisir le code d’état en fonction du type d’exception

Une nouvelle option disponible lors de la configuration de ExceptionHandlerMiddleware permet aux développeurs d’applications de choisir le code d’état à retourner lorsqu’une exception se produit au cours de la gestion des requêtes. La nouvelle option modifie le code d’état défini dans la réponse ProblemDetails provenant de ExceptionHandlerMiddleware.

app.UseExceptionHandler(new ExceptionHandlerOptions
{
    StatusCodeSelector = ex => ex is TimeoutException
        ? StatusCodes.Status503ServiceUnavailable
        : StatusCodes.Status500InternalServerError,
});

Désactiver les métriques HTTP sur certains points de terminaison et requêtes

.NET 9 introduit la possibilité de désactiver les métriques HTTP pour des points de terminaison et des requêtes spécifiques. La désactivation de l'enregistrement des métriques est bénéfique pour les points de terminaison fréquemment appelés par des systèmes automatisés, tels que les contrôles d’intégrité. L’enregistrement des métriques pour ces requêtes est généralement inutile.

Les requêtes HTTP vers un point de terminaison peuvent être exclues des métriques en ajoutant des métadonnées. Un des deux éléments suivants :

  • Ajoutez l’attribut [DisableHttpMetrics] au contrôleur d’API Web, au hub SignalR ou au service gRPC.
  • Appelez DisableHttpMetrics lors du mappage des points de terminaison au démarrage de l’application :
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();

var app = builder.Build();
app.MapHealthChecks("/healthz").DisableHttpMetrics();
app.Run();

La propriété MetricsDisabled a été ajoutée à IHttpMetricsTagsFeature pour :

  • Scénarios avancés où une requête ne correspond pas à un point de terminaison.
  • La désactivation dynamique de la collecte de métriques pour des requêtes HTTP spécifiques
// Middleware that conditionally opts-out HTTP requests.
app.Use(async (context, next) =>
{
    var metricsFeature = context.Features.Get<IHttpMetricsTagsFeature>();
    if (metricsFeature != null &&
        context.Request.Headers.ContainsKey("x-disable-metrics"))
    {
        metricsFeature.MetricsDisabled = true;
    }

    await next(context);
});

Prise en charge de la protection des données pour la suppression des clés

Avant .NET 9, les clés de protection des données ne pouvaient pas être supprimées par conception, afin d’éviter la perte de données. Lorsqu’une clé est supprimée, ses données protégées ne peuvent plus être récupérées. Compte tenu de leur petite taille, l’accumulation de ces clés avait généralement un impact minimal. Toutefois, pour accommoder des services de très longue durée, nous avons introduit la possibilité de supprimer des clés. En règle générale, seules les anciennes clés devraient être supprimées. Supprimez uniquement des clés lorsque vous pouvez accepter le risque de perdre des données en échange d’économies de stockage. Nous vous recommandons de ne pas supprimer les clés de protection des données.

using Microsoft.AspNetCore.DataProtection.KeyManagement;

var services = new ServiceCollection();
services.AddDataProtection();

var serviceProvider = services.BuildServiceProvider();

var keyManager = serviceProvider.GetService<IKeyManager>();

if (keyManager is IDeletableKeyManager deletableKeyManager)
{
    var utcNow = DateTimeOffset.UtcNow;
    var yearAgo = utcNow.AddYears(-1);

    if (!deletableKeyManager.DeleteKeys(key => key.ExpirationDate < yearAgo))
    {
        Console.WriteLine("Failed to delete keys.");
    }
    else
    {
        Console.WriteLine("Old keys deleted successfully.");
    }
}
else
{
    Console.WriteLine("Key manager does not support deletion.");
}

Le middleware prend en charge l’ID à clé.

Le middleware prend désormais en charge le Keyed DI dans le constructeur et la méthode Invoke/InvokeAsync :

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<MySingletonClass>("test");
builder.Services.AddKeyedScoped<MyScopedClass>("test2");

var app = builder.Build();
app.UseMiddleware<MyMiddleware>();
app.Run();

internal class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next,
        [FromKeyedServices("test")] MySingletonClass service)
    {
        _next = next;
    }

    public Task Invoke(HttpContext context,
        [FromKeyedServices("test2")]
            MyScopedClass scopedService) => _next(context);
}

Faire confiance au certificat de développement HTTPS ASP.NET Core sur Linux

Sur les distributions Linux basées sur Ubuntu et Fedora, dotnet dev-certs https --trust configure désormais le certificat de développement HTTPS ASP.NET Core comme certificat approuvé pour :

  • Navigateurs Chromium, par exemple Google Chrome, Microsoft Edge et Chromium
  • Mozilla Firefox et navigateurs dérivés de Mozilla
  • API .NET, par exemple, HttpClient

Auparavant, --trust ne fonctionnait que sous Windows et macOS. La confiance dans les certificats est appliquée par utilisateur.

Pour établir la confiance dans OpenSSL, l’outil dev-certs :

  • Place le certificat dans ~/.aspnet/dev-certs/trust
  • Exécute une version simplifiée de l’outil c_rehash d’OpenSSL sur l’annuaire.
  • Demande à l’utilisateur de mettre à jour la variable d’environnement SSL_CERT_DIR

Pour établir la confiance dans dotnet, l’outil place le certificat dans le magasin de certificats My/Root.

Pour établir la confiance dans les bases de données NSS, le cas échéant, l’outil recherche dans le répertoire personnel les profils Firefox, ~/.pki/nssdb et ~/snap/chromium/current/.pki/nssdb. Pour chaque répertoire trouvé, l’outil ajoute une entrée à nssdb.

Modèles mis à jour avec les dernières versions de Bootstrap, jQuery et jQuery Validation.

Les modèles et bibliothèques de projet core ASP.NET ont été mis à jour pour utiliser les dernières versions de Bootstrap, jQuery et jQuery Validation, en particulier :

  • Bootstrap 5.3.3
  • jQuery 3.7.1
  • jQuery Validation 1.21.0

Modifications majeures

Utilisez les articles des modifications cassants dans .NET pour rechercher les modifications cassants qui peuvent s’appliquer lors de la mise à niveau d’une application vers une version plus récente de .NET.