Meilleures pratiques ASP.NET Core

Par Mike Rousos

Cet article fournit des instructions pour optimiser les performances et la fiabilité des applications ASP.NET Core.

Mettre en cache de manière agressive

La mise en cache est abordée dans plusieurs parties de cet article. Pour plus d’informations, consultez Vue d’ensemble de la mise en cache dans ASP.NET Core.

Comprendre les chemins de code chaud

Dans cet article, un chemin de code chaud est défini comme un chemin de code qui est fréquemment appelé et où une grande partie du temps d’exécution se produit. Les chemins de code chaud limitent généralement le scale-out et les performances des applications. Ils sont abordés dans plusieurs parties de cet article.

Évitez les appels bloquants

Les applications ASP.NET Core doivent être conçues pour traiter de nombreuses demandes simultanément. Les API asynchrones permettent à un petit pool de threads de gérer des milliers de requêtes simultanées sans attendre lors d’appels bloquants. Au lieu d’attendre la fin d’une tâche synchrone de longue durée, le thread peut travailler sur une autre requête.

Un problème de performances courant dans les applications ASP.NET Core est lié aux appels bloquants qui pourraient être asynchrones. De nombreux appels bloquants synchrones conduisent à une défaillance du pool de conversation et à des temps de réponse qui se dégradent.

Ne bloquez pas l’exécution asynchrone en appelant Task.Wait ou Task<TResult>.Result. N’achetez pas de verrous dans les chemins de code courants. Les applications ASP.NET Core fonctionnent mieux lorsqu’elles sont conçues pour exécuter du code en parallèle. N’appelez pas Task.Run et attendez-le immédiatement. ASP.NET Core exécute déjà le code d’application sur des threads de pool de threads normaux, donc l’appel de Task.Run entraîne uniquement une planification de pool de threads superflue. Même si le code planifié bloquait un thread, Task.Run ne l’empêcherait pas.

  • Rendez les chemins de code chaud asynchrones.
  • Appelez les API d’accès aux données, d’E/S et d’opérations de longue durée de manière asynchrone si une API asynchrone est disponible.
  • N’utilisez pas Task.Run pour rendre une API synchrone asynchrone.
  • Rendez les actions de contrôleur/Razor Page asynchrones. L’ensemble de la pile des appels est asynchrone afin de tirer parti des modèles async/await.

Un profileur, tel que PerfView, peut être utilisé pour rechercher les threads ajoutés fréquemment au pool de threads. L’événement Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start indique qu’un thread a été ajouté au pool de threads.

Retourner des collections volumineuses sur plusieurs pages plus petites

Une page web ne doit pas charger de grandes quantités de données en même temps. Lorsque vous retournez une collection d’objets, déterminez si cela peut entraîner des problèmes de performances. Déterminez si la conception peut produire les résultats médiocres suivants :

  • Une consommation élevée de OutOfMemoryException ou de la mémoire
  • L’insuffisance du pool de threads (consultez les remarques suivantes sur IAsyncEnumerable<T>)
  • Des temps de réponse lent
  • Un nettoyage de la mémoire fréquent

Ajoutez la pagination pour atténuer les scénarios précédents. À l’aide des paramètres de taille de page et d’index de page, les développeurs doivent favoriser la conception du retour d’un résultat partiel. Lorsqu’un résultat exhaustif est requis, la pagination doit être utilisée pour remplir de manière asynchrone des lots de résultats afin d’éviter le verrouillage des ressources du serveur.

Pour plus d’informations sur la pagination et la limitation du nombre d’enregistrements retournés, consultez :

Retourner IEnumerable<T> ou IAsyncEnumerable<T>

Retourner IEnumerable<T> à partir d’une action entraîne une itération de collection synchrone par le sérialiseur. Le résultat est le blocage des appels et un risque d’insuffisance de pool de threads. Pour éviter l’énumération synchrone, utilisez ToListAsync avant de retourner l’énumérable.

À partir de ASP.NET Core 3.0, IAsyncEnumerable<T> peut être utilisé comme alternative à IEnumerable<T> qui énumère de manière asynchrone. Pour plus d’informations, consultez Types de retour d’action de contrôleur.

Réduire les allocations d’objets volumineux

Le récupérateur de mémoire .NET Core gère automatiquement l’allocation et la libération de mémoire dans les applications ASP.NET Core. Le nettoyage automatique de la mémoire signifie généralement que les développeurs n’ont pas besoin de se soucier de la façon ou du moment où la mémoire est libérée. Toutefois, le nettoyage des objets non référencés prend du temps processeur. Les développeurs doivent donc réduire l’allocation d’objets dans les chemins de code chaud. Le nettoyage de la mémoire est particulièrement coûteux sur les objets volumineux (> 85 000 octets). Les objets volumineux sont stockés sur le tas d’objets volumineux et nécessitent un nettoyage complet de la mémoire (génération 2). Contrairement aux collections de génération 0 et de génération 1, une collection de génération 2 nécessite une suspension temporaire de l’exécution de l’application. L’allocation et la dé-allocation fréquentes d’objets volumineux peuvent entraîner des performances incohérentes.

Recommandations :

  • Envisagez de mettre en cache des objets volumineux qui sont fréquemment utilisés. La mise en cache d’objets volumineux évite d’avoir des allocations coûteuses.
  • Effectuez des mémoires tampons de pool à l’aide d’un ArrayPool<T> pour stocker de grands groupes.
  • N’allouez pas de nombreux objets volumineux à courte durée de vie sur des chemins de code chaud.

Les problèmes de mémoire, comme le précédent, peuvent être diagnostiqués en examinant les statistiques de garbage collection (GC) dans PerfView et en examinant :

  • Temps de pause du nettoyage de la mémoire.
  • Quel pourcentage du temps processeur est consacré au nettoyage de la mémoire.
  • Nombre de nettoyages de la mémoire de génération 0, 1 et 2.

Pour plus d’informations, consultez Garbage Collection et Performances.

Optimiser l’accès aux données et les E/S

Les interactions avec un magasin de données et d’autres services distants sont souvent les parties les plus lentes d’une application ASP.NET Core. La lecture et l’écriture efficaces des données sont essentielles à de bonnes performances.

Recommandations :

  • Appelez toutes les API d’accès aux données de manière asynchrone.
  • Ne récupérez pas plus de données que nécessaire. Écrivez des requêtes pour retourner uniquement les données nécessaires pour la requête HTTP actuelle.
  • Envisagez la mise en cache des données fréquemment consultées récupérées à partir d’une base de données ou d’un service distant si des données légèrement obsolètes sont acceptables. Selon le scénario, utilisez un MemoryCache ou un DistributedCache. Pour plus d’informations, consultez Mise en cache des réponses dans ASP.NET Core.
  • Réduisez les boucles réseau. L’objectif est de récupérer les données requises au cours d’un seul appel plutôt que dans plusieurs appels.
  • Utilisezdes requêtes sans suivi dans Entity Framework Core lors de l’accès aux données à des fins de lecture seule. EF Core peut retourner les résultats des requêtes sans suivi plus efficacement.
  • Filtrez et agrégez les requêtes LINQ (avec des instructions .Where, .Selectou .Sum, par exemple) afin que le filtrage soit effectué par la base de données.
  • Considérez que EF Core résout certains opérateurs de requête sur le client, ce qui peut entraîner une exécution inefficace des requêtes. Pour plus d’informations, consultez Problèmes de performances d’évaluation des clients.
  • N’utilisez pas de requêtes de projection sur les collections, ce qui peut entraîner l’exécution de requêtes SQL « N + 1 ». Pour plus d’informations, consultez Optimisation des sous-requêtes corrélées.

Les approches suivantes peuvent améliorer les performances dans les applications à grande échelle :

Nous vous recommandons de mesurer l’impact des approches hautes performances précédentes avant de valider la base de code. La complexité supplémentaire des requêtes compilées peut ne pas justifier l’amélioration des performances.

Les problèmes de requête peuvent être détectés en examinant le temps passé à accéder aux données avec Application Insights ou avec des outils de profilage. La plupart des bases de données mettent également à disposition des statistiques concernant les requêtes fréquemment exécutées.

Connexions du pool HTTP avec HttpClientFactory

Bien que HttpClient implémente l’interface IDisposable, il est conçu pour être réutilisé. Les instances HttpClient fermées laissent les sockets ouverts à l’état TIME_WAIT pendant une courte période. Si un chemin de code qui crée et supprime des objets HttpClient est fréquemment utilisé, l’application peut épuiser les sockets disponibles. HttpClientFactory a été introduit dans ASP.NET Core 2.1 comme solution à ce problème. Il gère le regroupement des connexions HTTP pour optimiser les performances et la fiabilité. Pour plus d’informations, consultez Utiliser HttpClientFactory pour implémenter des requêtes HTTP résilientes.

Recommandations :

Faire que les chemins de code courants restent rapides

Vous voulez que tout votre code soit rapide. Les chemins de code fréquemment appelés sont les plus critiques à optimiser. Il s’agit notamment des paramètres suivants :

  • Les composants d’intergiciels dans le pipeline de traitement des demandes de l’application, en particulier les intergiciels s’exécutent tôt dans le pipeline. Ces composants ont un impact important sur les performances.
  • Code exécuté pour chaque requête ou plusieurs fois par requête. Par exemple, la journalisation personnalisée, les gestionnaires d’autorisation ou l’initialisation de services temporaires.

Recommandations :

Effectuer des tâches de longue durée en dehors des requêtes HTTP

La plupart des demandes adressées à une application ASP.NET Core peuvent être gérées par un contrôleur ou un modèle de page appelant les services nécessaires et renvoyant une réponse HTTP. Pour certaines demandes qui impliquent des tâches de longue durée, il est préférable de rendre l’ensemble du processus de réponse de requête asynchrone.

Recommandations :

  • N’attendez pas que les tâches de longue durée se terminent dans le cadre du traitement des requêtes HTTP ordinaires.
  • Envisagez de gérer les demandes de longue durée avec des services en arrière-plan ou hors processus avec une fonction Azure. L’exécution d’un travail hors processus est particulièrement utile pour les tâches qui requièrent un traitement intense de la part du processeur.
  • Utilisez des options de communication en temps réel, telles que SignalR, pour communiquer avec les clients de manière asynchrone.

Réduire les ressources du client

Les applications ASP.NET Core avec des front-ends complexes servent fréquemment de nombreux fichiers JavaScript, CSS ou image. Les performances des demandes de chargement initiales peuvent être améliorées par :

  • Regroupement, pour combiner plusieurs fichiers en un seul.
  • Minimisation, pour réduire la taille des fichiers en supprimant les espaces blancs et les commentaires.

Recommandations :

  • Suivez les instructions de regroupement et de minimisation, qui mentionnent les outils compatibles et montrent comment utiliser la balise environment d’ASP.NET Core pour gérer les environnements Development etProduction.
  • Envisagez d’autres outils tiers, tels que Webpack, pour la gestion des ressources clientes complexes.

Compresser les réponses

La réduction de la taille de la réponse augmente généralement la réactivité d’une application, souvent de façon spectaculaire. L’un des moyens de réduire les tailles de charge utile consiste à compresser les réponses d’une application. Pour plus d’informations, consultez Compression des réponses.

Utiliser la dernière version d’ASP.NET Core

Chaque nouvelle version d’ASP.NET Core inclut des améliorations des performances. Les optimisations dans .NET Core et ASP.NET Core signifient que les versions plus récentes surpassent généralement les versions antérieures. Par exemple, .NET Core 2.1 a ajouté la prise en charge des expressions régulières compilées et a bénéficié de Span<T>. ASP.NET Core 2.2 a ajouté la prise en charge de HTTP/2. ASP.NET Core 3.0 ajoute de nombreuses améliorations qui réduisent l’utilisation de la mémoire et améliorent le débit. Si les performances sont capitales, envisagez une mise à niveau vers la version actuelle d’ASP.NET Core.

Réduire les exceptions

Les exceptions doivent être rares. La levée et l’interception d’exceptions sont lentes par rapport à d’autres modèles de flux de code. Pour cette raison, les exceptions ne doivent pas être utilisées pour contrôler le flux normal du programme.

Recommandations :

  • N’utilisez pas la levée ou l’interception d’exceptions comme moyen de flux de programme normal, en particulier dans les chemins de code chaud.
  • Incluez la logique dans l’application pour détecter et gérer les conditions qui provoqueraient une exception.
  • Levez ou interceptez des exceptions pour des conditions inhabituelles ou inattendues.

Les outils de diagnostic d’application, tels qu’Application Insights, permettent d’identifier dans une application les exceptions courantes qui peuvent affecter les performances.

Éviter la lecture ou l’écriture synchrone sur le corps HttpRequest/HttpResponse

Toutes les E/S dans ASP.NET Core sont asynchrones. Les serveurs implémentent l’interface Stream, qui a des surcharges synchrones et asynchrones. Il faut préférer les éléments asynchrones pour éviter de bloquer les threads de pool de threads. Le blocage des threads peut entraîner une insuffisance du pool de threads.

Ne procédez pas comme suit : l’exemple suivant utilise ReadToEnd. Il empêche le thread actuel d’attendre le résultat. Il s’agit d’un exemple de synchronisation sur asynchronisation.

public class BadStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public ActionResult<ContosoData> Get()
    {
        var json = new StreamReader(Request.Body).ReadToEnd();

        return JsonSerializer.Deserialize<ContosoData>(json);
    }
}

Dans le code précédent, Get lit de manière synchrone tout le corps de la requête HTTP dans la mémoire. Si le client se charge lentement, l’application effectue la synchronisation sur asynchronisation. L’application ne se synchronise pas sur asynchronisation, car Kestrel ne prend PAS en charge les lectures synchrones.

Procédez comme suit : l’exemple suivant utilise ReadToEndAsync et ne bloque pas le thread lors de la lecture.

public class GoodStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public async Task<ActionResult<ContosoData>> Get()
    {
        var json = await new StreamReader(Request.Body).ReadToEndAsync();

        return JsonSerializer.Deserialize<ContosoData>(json);
    }

}

Le code précédent lit de manière asynchrone tout le corps de la requête HTTP dans la mémoire.

Avertissement

Si la requête est volumineuse, la lecture de l’ensemble du corps de la requête HTTP dans la mémoire peut entraîner une condition de mémoire insuffisante (OOM). Une mémoire insuffisante peut entraîner un déni de service. Pour plus d’informations, consultez Éviter de lire des corps de requêtes volumineux ou des corps de réponse en mémoire dans cet article.

Procédez comme suit : l’exemple suivant est entièrement asynchrone et utilise un corps de requête non mis en mémoire tampon :

public class GoodStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public async Task<ActionResult<ContosoData>> Get()
    {
        return await JsonSerializer.DeserializeAsync<ContosoData>(Request.Body);
    }
}

Le code précédent désérialise de façon asynchrone le corps de la requête en un objet C#.

Préférer ReadFormAsync à Request.Form

Utilisez HttpContext.Request.ReadFormAsync au lieu de HttpContext.Request.Form. HttpContext.Request.Form peut être lu en toute sécurité uniquement si les conditions suivantes sont réunies :

  • Le formulaire a été lu par un appel à ReadFormAsync, et
  • La valeur du formulaire mis en cache est en cours de lecture à l’aide de HttpContext.Request.Form

Ne procédez pas comme suit : l’exemple suivant utilise HttpContext.Request.Form. HttpContext.Request.Form utilise la synchronisation sur asynchronisation et peut entraîner une insuffisance du pool de threads.

public class BadReadController : Controller
{
    [HttpPost("/form-body")]
    public IActionResult Post()
    {
        var form =  HttpContext.Request.Form;

        Process(form["id"], form["name"]);

        return Accepted();
    }

Procédez comme suit : l’exemple suivant utilise HttpContext.Request.ReadFormAsync pour lire le corps du formulaire de manière asynchrone.

public class GoodReadController : Controller
{
    [HttpPost("/form-body")]
    public async Task<IActionResult> Post()
    {
       var form = await HttpContext.Request.ReadFormAsync();

        Process(form["id"], form["name"]);

        return Accepted();
    }

Éviter de lire des corps de requêtes volumineux ou des corps de réponse en mémoire

Dans .NET, chaque allocation d’objets supérieure à 85,000 octets se retrouve dans le tas d’objets volumineux (LOH, large object heap). Les objets volumineux sont coûteux pour deux raisons :

  • Le coût d’allocation est élevé, car la mémoire d’un objet volumineux nouvellement alloué doit être effacée. Le CLR garantit que la mémoire de tous les objets nouvellement alloués est effacée.
  • LOH est collecté avec le reste du tas. LOH nécessite un nettoyage de la mémoire complet ou un nettoyage gen2.

Ce billet de blog décrit le problème de manière succincte :

Lorsqu’un objet volumineux est alloué, il est marqué comme objet Gen 2. Il ne s’agit pas d’un objet Gen 0 comme pour les petits objets. Les conséquences sont que si vous n’avez plus de mémoire dans LOH, GC (garbage collection) nettoie l’ensemble du tas managé, pas seulement LOH. Il nettoie donc Gen 0, Gen 1 et Gen 2 y compris LOH. C’est ce qu’on appelle le nettoyage de la mémoire (ou garbage collection) complet et c’est le nettoyage de la mémoire le plus chronophage. Pour de nombreuses applications, il peut être acceptable. Mais certainement pas pour les serveurs web hautes performances, où peu de mémoires tampons volumineuses sont nécessaires pour gérer une requête web moyenne (lecture à partir d’un socket, décompression, décodage JSON, etc.).

Stockage d’une requête volumineuse ou d’un corps de réponse dans un seul byte[] ou string :

  • Peut entraîner une épuisement rapide de l’espace dans le LOH.
  • Peut entraîner des problèmes de performances pour l’application en raison de l’exécution de GC complets.

Utilisation d’une API de traitement de données synchrone

Lors de l’utilisation d’un sérialiseur/désérialiseur qui prend uniquement en charge les lectures et écritures synchrones (par exemple, Json.NET) :

  • Mettez les données en mémoire tampon de façon asynchrone avant de les transmettre au sérialiseur/désérialiseur.

Avertissement

Si la requête est volumineuse, cela peut entraîner une condition de mémoire insuffisante (OOM). Une mémoire insuffisante peut entraîner un déni de service. Pour plus d’informations, consultez Éviter de lire des corps de requêtes volumineux ou des corps de réponse en mémoire dans cet article.

ASP.NET Core 3.0 utilise System.Text.Json par défaut pour la sérialisation JSON. System.Text.Json:

  • Lit et écrit JSON de manière asynchrone.
  • Est optimisé pour le texte UTF-8.
  • Est généralement plus performant que Newtonsoft.Json.

Ne pas stocker IHttpContextAccessor.HttpContext dans un champ

Le IHttpContextAccessor.HttpContext retourne le HttpContext de la requête active lors de l’accès à partir du thread de requête. Le IHttpContextAccessor.HttpContext ne doit pas être stocké dans un champ ou une variable.

Ne procédez pas comme suit : l’exemple suivant stocke le HttpContext dans un champ, puis tente de l’utiliser ultérieurement.

public class MyBadType
{
    private readonly HttpContext _context;
    public MyBadType(IHttpContextAccessor accessor)
    {
        _context = accessor.HttpContext;
    }

    public void CheckAdmin()
    {
        if (!_context.User.IsInRole("admin"))
        {
            throw new UnauthorizedAccessException("The current user isn't an admin");
        }
    }
}

Le code précédent capture fréquemment une valeur Null ou incorrecte HttpContext dans le constructeur.

Procédez comme suit : Dans l’exemple suivant :

  • Stocke le IHttpContextAccessor dans un champ.
  • Utilise le champ HttpContext au bon moment et recherche null.
public class MyGoodType
{
    private readonly IHttpContextAccessor _accessor;
    public MyGoodType(IHttpContextAccessor accessor)
    {
        _accessor = accessor;
    }

    public void CheckAdmin()
    {
        var context = _accessor.HttpContext;
        if (context != null && !context.User.IsInRole("admin"))
        {
            throw new UnauthorizedAccessException("The current user isn't an admin");
        }
    }
}

N’accédez pas à HttpContext à partir de plusieurs threads

HttpContext n’est pas thread‑safe. Accéder à HttpContext à partir de plusieurs threads en parallèle peut entraîner un comportement inattendu, tel que des blocages, des incidents et une altération des données.

Ne procédez pas comme suit : l’exemple suivant effectue trois requêtes parallèles et journalise le chemin d’accès de la requête entrante avant et après la requête HTTP sortante. Le chemin de la requête est accessible à partir de plusieurs threads, potentiellement en parallèle.

public class AsyncBadSearchController : Controller
{       
    [HttpGet("/search")]
    public async Task<SearchResults> Get(string query)
    {
        var query1 = SearchAsync(SearchEngine.Google, query);
        var query2 = SearchAsync(SearchEngine.Bing, query);
        var query3 = SearchAsync(SearchEngine.DuckDuckGo, query);

        await Task.WhenAll(query1, query2, query3);

        var results1 = await query1;
        var results2 = await query2;
        var results3 = await query3;

        return SearchResults.Combine(results1, results2, results3);
    }       

    private async Task<SearchResults> SearchAsync(SearchEngine engine, string query)
    {
        var searchResults = _searchService.Empty();
        try
        {
            _logger.LogInformation("Starting search query from {path}.", 
                                    HttpContext.Request.Path);
            searchResults = _searchService.Search(engine, query);
            _logger.LogInformation("Finishing search query from {path}.", 
                                    HttpContext.Request.Path);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed query from {path}", 
                             HttpContext.Request.Path);
        }

        return await searchResults;
    }

Procédez comme suit : l’exemple suivant copie toutes les données de la requête entrante avant d’effectuer les trois requêtes parallèles.

public class AsyncGoodSearchController : Controller
{       
    [HttpGet("/search")]
    public async Task<SearchResults> Get(string query)
    {
        string path = HttpContext.Request.Path;
        var query1 = SearchAsync(SearchEngine.Google, query,
                                 path);
        var query2 = SearchAsync(SearchEngine.Bing, query, path);
        var query3 = SearchAsync(SearchEngine.DuckDuckGo, query, path);

        await Task.WhenAll(query1, query2, query3);

        var results1 = await query1;
        var results2 = await query2;
        var results3 = await query3;

        return SearchResults.Combine(results1, results2, results3);
    }

    private async Task<SearchResults> SearchAsync(SearchEngine engine, string query,
                                                  string path)
    {
        var searchResults = _searchService.Empty();
        try
        {
            _logger.LogInformation("Starting search query from {path}.",
                                   path);
            searchResults = await _searchService.SearchAsync(engine, query);
            _logger.LogInformation("Finishing search query from {path}.", path);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed query from {path}", path);
        }

        return await searchResults;
    }

Ne pas utiliser le httpContext une fois la demande terminée

HttpContext est valide uniquement tant qu’il existe une requête HTTP active dans le pipeline ASP.NET Core. L’ensemble du pipeline ASP.NET Core est une chaîne asynchrone de délégués qui exécute chaque requête. Lorsque le Task retourné à partir de cette chaîne se termine, le HttpContext est recyclé.

Ne procédez pas comme suit : l’exemple suivant utilise async void, ce qui termine la requête HTTP lorsque la première await est atteinte :

  • L’utilisation de async void est TOUJOURS une mauvaise pratique dans les applications ASP.NET Core.
  • L’exemple de code accède au HttpResponse une fois la requête HTTP terminée.
  • L’accès en retard bloque le processus.
public class AsyncBadVoidController : Controller
{
    [HttpGet("/async")]
    public async void Get()
    {
        await Task.Delay(1000);

        // The following line will crash the process because of writing after the 
        // response has completed on a background thread. Notice async void Get()

        await Response.WriteAsync("Hello World");
    }
}

Procédez comme suit : l’exemple suivant retourne un Task à l’infrastructure, de sorte que la requête HTTP ne se termine pas tant que l’action n’est pas terminée.

public class AsyncGoodTaskController : Controller
{
    [HttpGet("/async")]
    public async Task Get()
    {
        await Task.Delay(1000);

        await Response.WriteAsync("Hello World");
    }
}

Ne pas capturer le httpContext dans les threads d’arrière-plan

Ne procédez pas comme suit : l’exemple suivant montre qu’une fermeture capture le HttpContext à partir de la propriété Controller. Il s’agit d’une mauvaise pratique, car l’élément de travail pourrait :

  • Procéder à l’exécution en dehors de l’étendue de la requête.
  • Essayer de lire le mauvais HttpContext.
[HttpGet("/fire-and-forget-1")]
public IActionResult BadFireAndForget()
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        var path = HttpContext.Request.Path;
        Log(path);
    });

    return Accepted();
}

Procédez comme suit : Dans l’exemple suivant :

  • Copie les données requises dans la tâche en arrière-plan pendant la requête.
  • Ne fait aucune référence au contrôleur.
[HttpGet("/fire-and-forget-3")]
public IActionResult GoodFireAndForget()
{
    string path = HttpContext.Request.Path;
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        Log(path);
    });

    return Accepted();
}

Les tâches en arrière-plan doivent être implémentées en tant que services hébergés. Pour plus d’informations, consultez Tâches en arrière-plan avec services hébergés.

Ne pas capturer les services injectés dans les contrôleurs sur les threads d’arrière-plan

Ne procédez pas comme suit : l’exemple suivant montre qu’une fermeture capture le DbContext à partir du paramètre d’action Controller. C’est une mauvaise pratique. L’élément de travail peut s’exécuter en dehors de l’étendue de la requête. Le ContosoDbContext est étendu à la requête, ce qui entraîne un ObjectDisposedException.

[HttpGet("/fire-and-forget-1")]
public IActionResult FireAndForget1([FromServices]ContosoDbContext context)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        context.Contoso.Add(new Contoso());
        await context.SaveChangesAsync();
    });

    return Accepted();
}

Procédez comme suit : Dans l’exemple suivant :

  • Injecte un IServiceScopeFactory afin de créer une étendue dans l’élément de travail en arrière-plan. IServiceScopeFactory est un singleton.
  • Crée une nouvelle étendue d’injection de dépendances dans le thread en arrière-plan.
  • Ne fait aucune référence au contrôleur.
  • Ne capture pas le ContosoDbContext à partir de la requête entrante.
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        await using (var scope = serviceScopeFactory.CreateAsyncScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();

            context.Contoso.Add(new Contoso());

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

Le code en surbrillance suivant :

  • Crée une étendue pour la durée de vie de l’opération en arrière-plan et résout les services à partir de celle-ci.
  • Utilise ContosoDbContext à partir de l’étendue correcte.
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        await using (var scope = serviceScopeFactory.CreateAsyncScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();

            context.Contoso.Add(new Contoso());

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

Ne modifiez pas le code ou les en-têtes du statut après le démarrage du corps de la réponse

ASP.NET Core ne met pas en mémoire tampon le corps de la réponse HTTP. La première fois que la réponse est écrite :

  • Les en-têtes sont envoyés avec cette partie du corps au client.
  • Il n’est plus possible de modifier les en-têtes de réponse.

Ne procédez pas comme suit : le code suivant tente d’ajouter des en-têtes de réponse une fois que la réponse a déjà démarré :

app.Use(async (context, next) =>
{
    await next();

    context.Response.Headers["test"] = "test value";
});

Dans le code précédent, context.Response.Headers["test"] = "test value"; lève une exception si next() a écrit dans la réponse.

Procédez comme suit : l’exemple suivant vérifie si la réponse HTTP a démarré avant de modifier les en-têtes.

app.Use(async (context, next) =>
{
    await next();

    if (!context.Response.HasStarted)
    {
        context.Response.Headers["test"] = "test value";
    }
});

Procédez comme suit : l’exemple suivant utilise HttpResponse.OnStarting pour définir les en-têtes avant que les en-têtes de réponse ne soient vidés sur le client.

Vérifier si la réponse n’a pas démarré permet d’inscrire un rappel qui sera appelé juste avant l’écriture des en-têtes de réponse. Vérifier si la réponse n’a pas démarré :

  • Permet d’ajouter ou de remplacer des en-têtes juste à temps.
  • Ne nécessite pas de connaissance du prochain intergiciel dans le pipeline.
app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        context.Response.Headers["someheader"] = "somevalue";
        return Task.CompletedTask;
    });

    await next();
});

Ne pas appeler next() si vous avez déjà commencé à écrire dans le corps de la réponse

Les composants s’attendent à être appelés uniquement s’ils peuvent gérer et modifier la réponse.

Utiliser l’hébergement in-process avec IIS

En utilisant l’hébergement in-process, une application ASP.NET Core s’exécute dans le même processus que son processus de travail IIS. L’hébergement in-process offre de meilleures performances que l’hébergement out-of-process parce que les requêtes n’ont pas de proxy sur l’adaptateur de bouclage. L’adaptateur de bouclage est une interface réseau qui retourne le trafic réseau sortant vers le même ordinateur. IIS s’occupe de la gestion des processus par l’intermédiaire du service d’activation des processus Windows (WAS).

Les projets sont par défaut le modèle d’hébergement in-process dans ASP.NET Core 3.0 et versions ultérieures.

Pour plus d’informations, consultez Héberger ASP.NET Core sur Windows avec IIS

Ne pas supposer que HttpRequest.ContentLength n’est pas nul

HttpRequest.ContentLength est nul si l’en-tête Content-Length n’est pas reçu. Dans ce cas, nul signifie que la longueur du corps de la requête n’est pas connue ; cela ne signifie pas que la longueur est égale à zéro. Étant donné que toutes les comparaisons avec nul (sauf ==) reviennent fausses, la comparaison Request.ContentLength > 1024, par exemple, peut renvoyer false lorsque la taille du corps de la requête est supérieure à 1024. Ne pas le savoir peut entraîner des failles de sécurité dans les applications. Vous pensez peut-être que vous vous protégez contre les demandes trop volumineuses quand ce n’est pas le cas.

Pour plus d’informations, consultez cette question sur StackOverflow.

Modèles d’application web fiables

Consultez Le modèle d’application web fiable for.NETvidéos YouTube et l’article pour obtenir des conseils sur la création d’une application ASP.NET Core moderne, fiable, performante, testable, économique et évolutive, que ce soit à partir de zéro ou en refactorisant une application existante.