Nouveautés d’ASP.NET Core 9.0

Cet article met en évidence les modifications les plus importantes dans ASP.NET Core 9.0 et fournit des liens vers la documentation appropriée.

Cet article a été mis à jour pour .NET 9 Preview 7.

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, notamment une 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 kit 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, consultez Créer une application .NET MAUIBlazor Hybrid avec une Blazor Web App.

Optimisation de la distribution des ressources statiques

MapStaticAssets est un nouvel intergiciel qui permet d’optimiser la distribution de ressources statiques dans n’importe quelle application ASP.NET Core, y compris les applications Blazor.

Pour plus d’informations, consultez l’une des ressources suivantes :

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éterminer l’emplacement d’exécution actuel du composant : cela peut être particulièrement utile pour le débogage et l’optimisation des performances du composant.
  • 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 minutage de reconnexion utilise une stratégie d’interruption (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 d’intervalle avant nouvelle tentative en spécifiant une fonction pour calculer l’intervalle avant nouvelle tentative, comme l’exemple de backoff exponentielle 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 Apps

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 de serveur et de 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 WebAssembly .NET.

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, il s’agit de 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’application Blazor Web App, qui peuvent être appelées dans les projets serveur et client pour ajouter cette fonctionnalité :

  • AddAuthenticationStateSerialization : ajoute les services nécessaires pour sérialiser l’état d’authentification sur le serveur.
  • AddAuthenticationStateDeserialization : ajoute les services nécessaires pour désérialiser l’état d’authentification sur le navigateur.

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 de Sécuriser les applications Blazor côté serveur ASP.NET Core :

Ajouter des pages de rendu statique côté serveur (SSR) à une application Blazor Web App interactive globale

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 page de composant Razor avec le nouvel attribut [ExcludeFromInteractiveRouting] affecté à la directive @attributeRazor :

@attribute [ExcludeFromInteractiveRouting]

L’application de l’attribut entraîne la sortie de 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 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 méthode d’extension HttpContext.AcceptsInteractiveRouting permet au composant de détecter si [ExcludeFromInteractiveRouting] est appliqué à la page active.

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

  • Les pages dépourvues de l’annotation [ExcludeFromInteractiveRouting] présentent 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 dotées de l’annotation [ExcludeFromInteractiveRouting] adoptent le SSR statique (null est affecté à PageRenderMode).
<!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 HttpContext.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 constructeurs

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 directive de stratégie de sécurité du contenu (CSP) frame-ancestors sur 'self', qui autorise uniquement l’incorporation de l’application dans un <iframe> de l’origine à partir de laquelle l’application est servie quand la compression est activée ou qu’une configuration est fournie pour le contexte WebSocket.

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 de 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’attributtype="range", qui crée une entrée de plage qui prend en charge la liaison de modèle et la validation de formulaire, généralement rendue en tant que curseur ou contrôle de numérotation 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; }
    }
}

Plusieurs applications Blazor Web App par projet de serveur

La prise en charge de plusieurs applications Blazor Web App par projet de serveur sera prise en compte pour .NET 10 (novembre 2025).

Pour plus d’informations, consultez Prise en charge de plusieurs applications web Blazor par projet de serveur (dotnet/aspnetcore #52216).

SignalR

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

Prise en charge du type polymorphe dans SignalR Hubs

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

Amélioration des activités pour SignalR

SignalR dispose désormais d’une ActivitySource nommée Microsoft.AspNetCore.SignalR.Server qui é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 de méthode hub n’ont pas de parent. Cela signifie qu’elles ne sont pas regroupées sous la connexion SignalR longue durée.

L’exemple suivant utilise le tableau de bord .NET 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 :

// 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())
        {
            // We want to view all traces in development
            tracing.SetSampler(new AlwaysOnSampler());
        }

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

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

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

SignalR prend en charge le découpage 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 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 avec l’AOT natif (PublishAot). L’utilisation de hubs fortement typés avec l’AOT natif entraîne des avertissements lors de la génération et de la publication, ainsi qu’une exception d’exécution. L’utilisation de hubs fortement typés avec découpage (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 pour les API minimales.

Ajout de InternalServerError et InternalServerError<TValue> à TypedResults

La classe TypedResults est un véhicule utile pour retourner des réponses basées sur un code d’état HTTP fortement typé à partir d’une API minimale. TypedResults inclut désormais des types et des méthodes de fabrique pour renvoyer des réponses « Erreur de serveur interne 500 » à partir de 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 des groupes de gammes

Les méthodes d’extension ProducesProblem et ProducesValidationProblem ont été mises à jour pour prendre en charge leur utilisation sur des groupes de gammes. Ces méthodes indiquent que tous les points de terminaison d’un groupe de gammes peuvent retourner des réponses ValidationProblemDetails ou ProblemDetails pour les besoins 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);

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 Preview, ASP.NET Core fournit une prise en charge intégrée de la génération de documents OpenAPI représentant des API basées sur contrôleur ou minimales via le package Microsoft.AspNetCore.OpenApi.

Le code mis en évidence suivant 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 --prerelease

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 --prerelease

Dans le fichier projet de l’application, ajoutez les éléments suivants :

<PropertyGroup>
  <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
  <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
</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 documents et d’opérations et permet 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.

Améliorations de la complétion IntelliSense pour le package OpenAPI

La prise en charge d’OpenAPI d’ASP.NET Core est désormais plus accessible et plus conviviale. Les API OpenAPI sont fournies sous forme de package indépendant, à part du framework partagé. Jusqu’à présent, cela signifiait que les développeurs n’avaient pas les fonctionnalités pratiques de complétion de code comme IntelliSense pour les API OpenAPI.

Réalisant ce manque, nous avons introduit un nouveau fournisseur de complétion et un correcteur de code. Ces outils sont conçus pour faciliter la découverte et l’utilisation des API OpenAPI et pour permettre aux développeurs d’intégrer plus facilement OpenAPI dans leurs projets. Le fournisseur de complétion propose des suggestions de code en temps réel, tandis que le correcteur de code aide à corriger les erreurs courantes et à améliorer l’utilisation des API. Cette amélioration entre dans le cadre de notre engagement continu pour améliorer l’expérience des développeurs et simplifier les workflows liés aux API.

Lorsqu’un utilisateur tape une instruction où une API liée à OpenAPI est disponible, le fournisseur de complétion affiche une recommandation pour l’API. Par exemple, dans les captures d’écran suivantes, les complétions pour AddOpenApi et MapOpenApi sont fournies lorsqu’un utilisateur entre une instruction d’appel sur un type pris en charge, par exemple IEndpointConventionBuilder :

Complétions OpenAPI

Lorsque la complétion est acceptée et que le package Microsoft.AspNetCore.OpenApi n’est pas installé, un correcteur de code fournit un raccourci pour installer automatiquement la dépendance dans le projet.

Installation de package automatique

Prise en charge des attributs [Required] et [DefaultValue] sur les paramètres et les propriétés

Lorsque les attributs [Required] et [DefaultValue] sont appliqués sur des paramètres ou des propriétés au sein de types complexes, l’implémentation OpenAPI les mappe aux propriétés required et default dans le document OpenAPI associé au schéma de paramètre ou de type.

Par exemple, l’API suivante produit le schéma associé pour le type Todo.

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapPost("/todos", (Todo todo) => { });

app.Run();

class Todo
{
	public int Id { get; init; }
	public required string Title { get; init; }
	[DefaultValue("A new todo")]
	public required string Description { get; init; }
	[Required]
	public DateTime CreatedOn { get; init; }
}
{
	"required": [
	  "title",
	  "description",
	  "createdOn"
	],
	"type": "object",
	"properties": {
	  "id": {
	    "type": "integer",
	    "format": "int32"
	  },
	  "title": {
	    "type": "string"
	  },
	  "description": {
	    "type": "string",
	    "default": "A new todo"
	  },
	  "createdOn": {
	    "type": "string",
	    "format": "date-time"
	  }
	}
}

Prise en charge des transformateurs de schéma sur des documents OpenAPI

La prise en charge d’OpenAPI intégrée est désormais fournie avec la prise en charge des transformateurs de schémas qui peuvent être utilisés pour modifier les schémas générés par System.Text.Json et l’implémentation OpenAPI. Comme les transformateurs de document et d’opération, les transformateurs de schéma peuvent être inscrits sur l’objet OpenApiOptions. Par exemple, l’exemple de code suivant illustre l’utilisation d’un transformateur de schéma pour ajouter un exemple au schéma d’un type.

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Microsoft.OpenApi.Any;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.UseSchemaTransformer((schema, context, cancellationToken) =>
    {
        if (context.Type == typeof(Todo))
        {
            schema.Example = new OpenApiObject
            {
                ["id"] = new OpenApiInteger(1),
                ["title"] = new OpenApiString("A short title"),
                ["description"] = new OpenApiString("A long description"),
                ["createdOn"] = new OpenApiDateTime(DateTime.Now)
            };
        }
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapPost("/todos", (Todo todo) => { });

app.Run();

class Todo
{
	public int Id { get; init; }
	public required string Title { get; init; }
	[DefaultValue("A new todo")]
	public required string Description { get; init; }
	[Required]
	public DateTime CreatedOn { get; init; }
}

Améliorations apportées aux API d’inscription de transformateurs dans Microsoft.AspNetCore.OpenApi

Les transformateurs OpenAPI prennent en charge la modification de document OpenAPI, des opérations dans le document ou des schémas associés aux types dans l’API. Les API permettant d’inscrire des transformateurs sur un document OpenAPI proposent diverses options pour l’inscription de transformateurs.

Auparavant, les API suivantes étaient disponibles pour l’inscription de transformateurs :

OpenApiOptions UseTransformer(Func<OpenApiDocument, OpenApiDocumentTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions UseTransformer(IOpenApiDocumentTransformer transformer)
OpenApiOptions UseTransformer<IOpenApiDocumentTransformer>()
OpenApiOptions UseSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task>)
OpenApiOptions UseOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task>)

Le nouvel ensemble d’API est le suivant :

OpenApiOptions AddDocumentTransformer(Func<OpenApiDocument, OpenApiDocumentTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddDocumentTransformer(IOpenApiDocumentTransformer transformer)
OpenApiOptions AddDocumentTransformer<IOpenApiDocumentTransformer>()

OpenApiOptions AddSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddSchemaTransformer(IOpenApiSchemaTransformer transformer)
OpenApiOptions AddSchemaTransformer<IOpenApiSchemaTransformer>()

OpenApiOptions AddOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddOperationTransformer(IOpenApiOperationTransformer transformer)
OpenApiOptions AddOperationTransformer<IOpenApiOperationTransformer>()

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

Désormais, la nouvelle intégration d’OpenAPI dans ASP.NET Core prend aussi en charge le découpage et l’AOT natif.

Démarrage

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 --prerelease

Pour cette préversion, vous devez également ajouter le dernier package Microsoft.OpenAPI pour éviter les avertissements de découpage.

dotnet add package Microsoft.OpenApi

Mettez à 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

L’application publie à l’aide de l’AOT natif sans avertissements.

Prise en charge des appels ProducesProblem et ProducesValidationProblem sur des groupes de gammes

Les méthodes d’extension ProducesProblem et ProducesValidationProblem ont été mises à jour pour les groupes de gammes. Ces méthodes peuvent être utilisées pour indiquer que tous les points de terminaison d’un groupe de gammes peuvent retourner des réponses ProblemDetails ou ValidationProblemDetails pour les besoins des métadonnées OpenAPI.

var app = WebApplication.Create();

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

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

app.Run();

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

Les types de résultats Problem et ValidationProblem prennent en charge la construction avec des valeurs IEnumerable<KeyValuePair<string, object?>>.

Avant .NET 9, la construction de types de résultats Problem et ValidationProblem dans des 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 !

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. Autrement dit, elle déplace les paramètres d’autorisation des URL de redirection du navigateur vers des appels http directs d’ordinateur à ordinateur sur le back end.

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

  • Afficher les paramètres d’autorisation, ce qui pourrait fuiter des informations d’identification personnelle
  • Falsifier ces paramètres, l’attaquant 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.

La PAR est prise en charge par un certain nombre de fournisseurs d’identity, notamment

Pour .NET 9, nous avons décidé d’activer la PAR par défaut si le document de découverte du fournisseur d’identity annonce la prise en charge de la PAR, car il devrait fournir une sécurité renforcée pour les fournisseurs qui le prennent en charge. Le document de découverte du fournisseur d’identity se trouve généralement à l’emplacement .well-known/openid-configuration. Si cela entraîne des problèmes, vous pouvez désactiver la 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 vous assurer que l’authentification réussit uniquement si la PAR est utilisée, utilisez plutôt PushedAuthorizationBehavior.Require. 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 et les versions antérieures, elle exige un rappel OnRedirectToIdentityProvider personnalisé ou une méthode BuildChallengeUrl substituée 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 des indicateurs d’authentification étendus 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

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

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

HybridCache est conçue comme un remplacement de dépôt pour l’utilisation de IDistributedCache et IMemoryCache existante, et elle fournit une API simple pour l’ajout de nouveau code de mise en cache. Elle fournit une API unifiée pour la mise en cache in-process et out-of-process.

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 de IDistributedCache configurée, le cas échéant, pour la mise en cache out-of-process secondaire, par exemple, à l’aide de Redis. Mais même sans IDistributedCache, le service HybridCache fournira toujours la mise en cache in-process et la protection « stampede ».

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 cohérence de thread, 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’usage donné est intrinsèquement thread-safe :

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

Dans de tels cas, informez HybridCache qu’il est sûr de réutiliser des instances en :

  • Marquant le type en tant que 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 et Microsoft.Extensions.Caching.SqlServer.

La sérialisation est configurée dans le cadre de l’inscription du service, avec prise en charge des sérialiseurs propres au type et généralisés via les méthodes WithSerializer et .WithSerializerFactory, mis en chaîne à 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 Preview 3 a ajouté des métadonnées de point de terminaison à la page d’exceptions 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 de l’exceptions du développeur, de légères améliorations destinées à faciliter le travail ont été identifiées. Elles ont été livrées dans la Preview 4 :

  • Meilleur habillage de 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 dans les designs modernes.
  • Tailles de table plus homogènes.

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

Page d’exceptions du développeur

Améliorations du débogage des dictionnaires

L’affichage de débogage des dictionnaires et d’autres collections clé-valeur a une disposition améliorée. La clé s’affiche 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’application 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.

Optimisation de la remise des ressources web statiques

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 un nouvel intergiciel qui permet d’optimiser la distribution de ressources statiques dans 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 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 génération 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 contenuETags : les Etags pour chaque ressource sont la chaîne codée Base64 du hachage du contenu SHA-256. 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 :

File Original 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 :

File Original 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és.

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

File Original 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 génération.
  • 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 MapStaticAssets Reduction
≅ 90 37,5 59 %

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.

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] substitué 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 still be useful
    // for endpoints that allow but don't require authenticated users.
    [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 l’établissement d’une liaison 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 d’en-têtes de requête dans les temps.
  • 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. Toutefois, les journaux peuvent être coûteux à générer et stocker, et il peut être difficile de trouver les informations appropriées 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ée de Kestrel a empêchées

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

Personnaliser les points de terminaison de canal nommé Kestrel

La prise en charge du canal nommé par Kestrel a été améliorée avec 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 :

  • Les 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 de 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 prendre en charge des services extrêmement longs, 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.");
}

L’intergiciel prend en charge l’injection de dépendances à clé.

L’intergiciel prend désormais en charge l’injection de dépendances à clé 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 de certificat 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 home des profils Firefox, ~/.pki/nssdb et ~/snap/chromium/current/.pki/nssdb. Pour chaque répertoire trouvé, l’outil ajoute une entrée à nssdb.