Procedure consigliate di ASP.NET Core

Note

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 10 di questo articolo.

Warning

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i Criteri di supporto per .NET e .NET Core. Per la versione corrente, vedere la versione .NET 10 di questo articolo.

Di Mike Rousos

Questo articolo fornisce linee guida per ottimizzare le prestazioni e l'affidabilità delle app ASP.NET Core.

Cache in modo aggressivo

La memorizzazione nella cache è descritta in diverse parti di questo articolo. Per altre informazioni, vedere Panoramica della memorizzazione nella cache in ASP.NET Core.

Comprendere i percorsi critici del codice

In questo articolo un percorso di codice critico viene definito come percorso di codice che viene spesso chiamato e in cui si verifica gran parte del tempo di esecuzione. I percorsi di codice ad accesso frequente limitano in genere la scalabilità orizzontale e le prestazioni delle app e sono descritti in diverse parti di questo articolo.

Evitare le chiamate di blocco

Le app ASP.NET Core devono essere progettate per elaborare più richieste contemporaneamente. Le API asincrone consentono a un piccolo pool di thread di gestire migliaia di richieste simultanee senza attendere chiamate bloccanti. Anziché attendere il completamento di un'attività sincrona a esecuzione prolungata, il thread può lavorare su un'altra richiesta.

Un problema di prestazioni comune nelle app ASP.NET Core è costituito da chiamate bloccanti che potrebbero essere asincrone. Molte chiamate di blocco sincrone causano la starvation del pool di thread e tempi di risposta degradati.

Non bloccare l'esecuzione asincrona chiamando Task.Wait o Task<TResult>.Result. Non acquisire lock in percorsi di codice comuni. Le app ASP.NET Core funzionano meglio quando sono progettate per eseguire il codice in parallelo. Non chiamare Task.Run e non attenderlo immediatamente. ASP.NET Core esegue già il codice dell'app nei normali thread del pool di thread, quindi la chiamata Task.Run comporta solo la pianificazione aggiuntiva del pool di thread non necessaria. Anche se il codice pianificato bloccherebbe un thread, Task.Run non lo impedisce.

  • Rendi asincroni i percorsi critici del codice.
  • Chiamare in modo asincrono le API di accesso ai dati, I/O e operazioni a esecuzione prolungata se è disponibile un'API asincrona.
  • Non usare Task.Run per rendere asincrona un'API sincrona.
  • Rendi asincrone le azioni della pagina controller/Razor. L'intero stack di chiamate è asincrono per trarre vantaggio dai modelli async/await.
  • Prendere in considerazione l'uso di broker di messaggi come bus di servizio di Azure per eseguire l'offload delle chiamate a esecuzione prolungata

Un profiler, ad esempio PerfView, può essere usato per trovare i thread aggiunti di frequente al pool di thread. L'evento Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start indica che un thread è stato aggiunto al pool di thread.

Suddividere raccolte di grandi dimensioni in più pagine più piccole

Una pagina Web non deve caricare grandi quantità di dati contemporaneamente. Quando si restituisce una raccolta di oggetti, valutare se potrebbe causare problemi di prestazioni. Determina se il design potrebbe produrre i seguenti risultati indesiderati:

Aggiungere la paginazione per attenuare gli scenari precedenti. Usando i parametri delle dimensioni della pagina e dell'indice di pagina, gli sviluppatori dovrebbero preferire un design che restituisca un risultato parziale. Quando è necessario un risultato esaustivo, la paginazione deve essere usata per popolare in modo asincrono batch di risultati per evitare di bloccare le risorse del server.

Per altre informazioni sul paging e sulla limitazione del numero di record restituiti, vedere:

Restituisce IEnumerable<T> o IAsyncEnumerable<T>

La restituzione IEnumerable<T> da un'azione comporta l'iterazione sincrona della raccolta da parte del serializzatore. Il risultato è il blocco delle chiamate e il potenziale rischio di starvation del pool di thread. Per evitare l'enumerazione sincrona, usare ToListAsync prima di restituire l'enumerable.

A partire da ASP.NET Core 3.0, IAsyncEnumerable<T> può essere usato come alternativa a IEnumerable<T> che enumera in modo asincrono. Per altre informazioni, vedere Tipi restituiti dalle azioni del controller.

Ridurre al minimo le allocazioni di oggetti di grandi dimensioni

.NET Garbage Collector gestisce automaticamente l'allocazione e il rilascio della memoria nelle app ASP.NET Core. La Garbage Collection automatica indica in genere che gli sviluppatori non devono preoccuparsi di come o quando viene liberata la memoria. Tuttavia, la pulizia degli oggetti senza riferimenti richiede tempo cpu, quindi gli sviluppatori dovrebbero ridurre al minimo l'allocazione di oggetti nei percorsi di codice ad accesso frequente. Garbage Collection è particolarmente costoso per oggetti di grandi dimensioni (>= 85.000 byte). Gli oggetti di grandi dimensioni vengono archiviati nell'heap di oggetti di grandi dimensioni e richiedono una Garbage Collection completa (generazione 2) per eseguire la pulizia. A differenza delle raccolte di generazione 0 e generazione 1, una raccolta di seconda generazione richiede una sospensione temporanea dell'esecuzione dell'app. L'allocazione frequente e la de-allocazione di oggetti di grandi dimensioni possono causare prestazioni incoerenti.

Recommendations:

  • Prendi in considerazione la memorizzazione nella cache di oggetti di grandi dimensioni usati di frequente. La memorizzazione nella cache di oggetti di grandi dimensioni impedisce allocazioni costose.
  • Raggruppare i buffer in pool usando un ArrayPool<T> oggetto per archiviare array di grandi dimensioni.
  • Non allocare molti oggetti di grandi dimensioni di breve durata nei percorsi critici del codice.

I problemi di memoria, ad esempio quelli precedenti, possono essere diagnosticati esaminando le statistiche di Garbage Collection (GC) in PerfView ed esaminando:

  • Tempo di pausa della garbage collection.
  • Quale percentuale del tempo del processore viene impiegata nella Garbage Collection?
  • Quante operazioni di Garbage Collection sono di generazione 0, 1 e 2?

Per altre informazioni, vedere Garbage Collection e prestazioni.

Ottimizzare l'accesso ai dati e l'I/O

Le interazioni con un archivio dati e altri servizi remoti sono spesso le parti più lente di un'app ASP.NET Core. La lettura e la scrittura dei dati in modo efficiente sono fondamentali per ottenere prestazioni ottimali.

Recommendations:

  • Chiamare tutte le API di accesso ai dati in modo asincrono.
  • Non recuperare più dati di quanto necessario. Scrivere query per restituire solo i dati necessari per la richiesta HTTP corrente.
  • Considerare la memorizzazione nella cache dei dati a cui si accede di frequente recuperati da un database o da un servizio remoto se dati leggermente non aggiornati sono accettabili. A seconda dello scenario, usare MemoryCache o DistributedCache. Per altre informazioni, vedere Memorizzazione nella cache delle risposte in ASP.NET Core.
  • Riduci al minimo i round trip di rete. L'obiettivo è recuperare i dati necessari in una singola chiamata anziché più chiamate.
  • Usarequery senza tracciamento in Entity Framework Core per l'accesso ai dati a scopo di sola lettura. EF Core può restituire i risultati delle query senza tracciamento in modo più efficiente.
  • Eseguire il filtro e l'aggregazione delle query LINQ (ad esempio con istruzioni .Where, .Select o .Sum) in modo che il filtro venga eseguito dal database.
  • Tieni presente che EF Core risolve alcuni operatori di query nel client, il che può causare un'esecuzione inefficiente della query. Per altre informazioni, vedere Problemi di prestazioni della valutazione client.
  • Non usare query di proiezione sulle raccolte, che possono comportare l'esecuzione di query SQL "N + 1". Per altre informazioni, vedere Ottimizzazione delle sottoquery correlate.

Gli approcci seguenti possono migliorare le prestazioni nelle app su larga scala:

È consigliabile misurare l'impatto degli approcci ad alte prestazioni precedenti prima di eseguire il commit della codebase. La complessità aggiuntiva delle query compilate potrebbe non giustificare il miglioramento delle prestazioni.

È possibile rilevare i problemi di query esaminando il tempo dedicato all'accesso ai dati con Application Insights o con gli strumenti di profilatura. La maggior parte dei database rende disponibili anche le statistiche relative alle query eseguite di frequente.

Gestisci il pooling delle connessioni HTTP con HttpClientFactory

Anche se HttpClient implementa l'interfaccia IDisposable , è progettata per il riutilizzo. Le istanze chiuse HttpClient lasciano aperti i socket nello TIME_WAIT stato per un breve periodo di tempo. Se viene usato di frequente un percorso di codice che crea ed elimina oggetti HttpClient , l'app può esaurire i socket disponibili. HttpClientFactory è stato introdotto in ASP.NET Core 2.1 come soluzione a questo problema. Gestisce il pool di connessioni HTTP per ottimizzare le prestazioni e l'affidabilità. Per altre informazioni, vedere Usare HttpClientFactory per implementare richieste HTTP resilienti.

Recommendations:

Mantenere veloci i percorsi di codice comuni

Si vuole che tutto il codice sia veloce. I percorsi di codice chiamati di frequente sono i più critici per l'ottimizzazione. Questi includono:

  • I componenti middleware nella pipeline di elaborazione delle richieste dell'app, in particolare i middleware eseguiti nelle prime fasi della pipeline. Questi componenti hanno un impatto notevole sulle prestazioni.
  • Codice eseguito per ogni richiesta o più volte per ogni richiesta. Ad esempio, registrazione personalizzata, gestori di autorizzazione o inizializzazione di servizi temporanei.

Recommendations:

  • Non usare componenti middleware personalizzati con attività a esecuzione prolungata.
  • Usare strumenti di profilatura delle prestazioni, ad esempio Strumenti di diagnostica di Visual Studio o PerfView, per identificare i percorsi di codice critici.

Completare attività con esecuzione prolungata all'esterno delle richieste HTTP

La maggior parte delle richieste a un'app ASP.NET Core può essere gestita da un controller o da un modello di pagina che chiama i servizi necessari e restituisce una risposta HTTP. Per alcune richieste che comportano attività a esecuzione prolungata, è preferibile rendere asincrono l'intero processo di richiesta-risposta.

Recommendations:

  • Non attendere il completamento delle attività a esecuzione prolungata come parte dell'elaborazione normale delle richieste HTTP.
  • Considera la gestione delle richieste a esecuzione prolungata con servizi in background o out-of-process eventualmente con una Azure Function e/o usando un broker di messaggi come bus di servizio di Azure. Il completamento del lavoro out-of-process è particolarmente utile per le attività a elevato utilizzo di CPU.
  • Usa opzioni di comunicazione in tempo reale, ad esempio SignalR, per comunicare con i client in modo asincrono.

Ridurre le risorse del client

Le app ASP.NET Core con front-end complessi spesso servono molti file JavaScript, CSS o image. Le prestazioni delle richieste di caricamento iniziali possono essere migliorate:

  • Bundling, che combina più file in uno.
  • Minimizzazione, che riduce le dimensioni dei file rimuovendo spazi vuoti e commenti.

Recommendations:

  • Usa le linee guida per la creazione di bundle e la minimizzazione, che indicano gli strumenti compatibili e illustrano come usare il tag environment di ASP.NET Core per gestire gli ambienti Development e Production.
  • Considera altri strumenti di terze parti, ad esempio Webpack, per una gestione complessa degli asset client.

Comprimere le risposte

La riduzione delle dimensioni della risposta aumenta in genere la velocità di risposta di un'app, spesso notevolmente. Un modo per ridurre le dimensioni del payload consiste nel comprimere le risposte di un'app. Per altre informazioni, vedere Compressione della risposta.

Usare la versione più recente di ASP.NET Core

Ogni nuova versione di ASP.NET Core include miglioramenti delle prestazioni. Le ottimizzazioni in .NET e ASP.NET Core indicano che le versioni più recenti in genere superano le versioni precedenti. Ad esempio, .NET Core 2.1 ha aggiunto il supporto per le espressioni regolari compilate e ha beneficiato di Span<T>. ASP.NET Core 2.2 ha aggiunto il supporto per HTTP/2. ASP.NET Core 3.0 aggiunge molti miglioramenti che riducono l'utilizzo della memoria e migliorano la velocità effettiva. Se le prestazioni sono prioritarie, prendere in considerazione l'aggiornamento alla versione corrente di ASP.NET Core.

Ridurre al minimo le eccezioni

Le eccezioni devono essere rare. Il sollevamento e l'intercettazione di eccezioni sono lenti rispetto ad altri modelli di flusso di codice. Per questo motivo, le eccezioni non devono essere usate per controllare il normale flusso del programma.

Recommendations:

  • Non usare la generazione o l'intercettazione di eccezioni come mezzo del normale flusso di programma, in particolare nei percorsi critici del codice.
  • Includi la logica nell'app per rilevare e gestire le condizioni che causerebbero un'eccezione.
  • Lancia o intercetta eccezioni per condizioni insolite o impreviste.

Gli strumenti di diagnostica delle app, ad esempio Application Insights, possono aiutare a identificare le eccezioni comuni in un'app che possono influire sulle prestazioni.

Evitare operazioni di lettura o scrittura sincrone nel corpo HttpRequest/HttpResponse

Tutte le operazioni di I/O in ASP.NET Core sono asincrone. I server implementano l'interfaccia Stream , che dispone sia di overload sincroni che asincroni. È consigliabile preferire quelli asincroni per evitare di bloccare i thread del pool di thread. Il blocco dei thread può causare l'esaurimento del thread pool.

Non eseguire questa operazione: Nell'esempio seguente viene utilizzato il ReadToEnd. Blocca il thread corrente per attendere il risultato. Questo è un esempio di sync over async.

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

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

Nel codice precedente, Get legge in modo sincrono l'intero corpo della richiesta HTTP in memoria. Se il client sta effettuando l'upload lentamente, l'app esegue sync over async. L'app esegue operazioni sincrone su operazioni asincrone perché KestrelNON supporta letture sincrone.

Eseguire questa operazione: l'esempio seguente usa ReadToEndAsync e non blocca il thread durante la lettura.

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

}

Il codice precedente legge in modo asincrono l'intero corpo della richiesta HTTP in memoria.

Warning

Se la richiesta è di grandi dimensioni, la lettura dell'intero corpo della richiesta HTTP in memoria potrebbe causare una condizione di memoria insufficiente (OOM). L'OOM può comportare un denial of service. Per altre informazioni, vedere la sezione Evitare di leggere in memoria il corpo delle richieste o il corpo delle risposte di grandi dimensioni in questo articolo.

Eseguire questa operazione: l'esempio seguente è completamente asincrono usando un corpo della richiesta non memorizzato nel buffer:

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

Il codice precedente de serializza in modo asincrono il corpo della richiesta in un oggetto C#.

Preferire ReadFormAsync a Request.Form

Usare HttpContext.Request.ReadFormAsync invece di HttpContext.Request.Form. HttpContext.Request.Form può essere letto in sicurezza solo alle condizioni seguenti:

  • Il modulo è stato letto da una chiamata a ReadFormAsynce
  • Il valore del modulo memorizzato nella cache viene letto usando HttpContext.Request.Form

Non farlo: L'esempio seguente usa HttpContext.Request.Form. HttpContext.Request.Form usa sync over async e può causare l'esaurimento del pool di thread.

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

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

        return Accepted();
    }

Eseguire questa operazione: nell'esempio seguente HttpContext.Request.ReadFormAsync viene utilizzato per leggere il corpo del modulo in modo asincrono.

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

Evitare di leggere corpi di richiesta di grandi dimensioni o corpi di risposta in memoria

In .NET ogni allocazione di oggetti maggiore o uguale a 85.000 byte finisce nell'heap di oggetti di grandi dimensioni (LOH). Gli oggetti di grandi dimensioni sono costosi in due modi:

  • Il costo di allocazione è elevato perché la memoria per un oggetto di grandi dimensioni appena allocato deve essere azzerata. CLR garantisce che la memoria per tutti gli oggetti appena allocati venga azzerata.
  • LOH viene raccolto insieme al resto dell'heap. LOH richiede un'operazione completa di Garbage Collection o raccolta Gen2.

Questo post di blog descrive il problema in modo conciso:

Quando viene allocato un oggetto di grandi dimensioni, viene contrassegnato come oggetto Gen 2. Non è Gen 0 come per gli oggetti di piccole dimensioni. Le conseguenze sono che se si esaurisce la memoria in LOH, GC pulisce l'intero heap gestito, non solo LOH. Quindi pulisce Gen 0, Gen 1 e Gen 2, LOH compreso. Questa operazione è denominata Garbage Collection completa ed è la procedura di Garbage Collection più dispendiosa in termini di tempo. Per molte applicazioni, può essere accettabile. Ma sicuramente non per i server Web ad alte prestazioni, in cui sono necessari pochi buffer di memoria di grandi dimensioni per gestire una richiesta Web media (leggere da un socket, decomprimere, decodificare JSON e altro ancora).

Archiviazione di un corpo di richiesta o risposta di grandi dimensioni in un singolo byte[] o string:

  • Può causare un esaurimento rapido dello spazio nel LOH.
  • Può causare problemi di prestazioni per l'app a causa dell'esecuzione di GC complete.

Lavorare con un'API sincrona di elaborazione dei dati

Quando si usa un serializzatore/de-serializzatore che supporta solo letture e scritture sincrone (ad esempio, Json.NET):

  • Memorizzare i dati in memoria in modo asincrono prima di passarli al serializzatore/de-serializzatore.

Warning

Se la richiesta è di grandi dimensioni, potrebbe causare una condizione di memoria insufficiente ( OOM). L'OOM può comportare un denial of service. Per altre informazioni, vedere la sezione Evitare di leggere in memoria il corpo delle richieste o il corpo delle risposte di grandi dimensioni in questo articolo.

ASP.NET Core 3.0 usa System.Text.Json per impostazione predefinita per la serializzazione JSON. System.Text.Json:

  • Legge e scrive JSON in modo asincrono.
  • È ottimizzato per il testo UTF-8.
  • In genere offre prestazioni più elevate rispetto a Newtonsoft.Json.

Non archiviare IHttpContextAccessor.HttpContext in un campo

IHttpContextAccessor.HttpContext restituisce l'oggetto HttpContext della richiesta attiva quando si accede dal thread della richiesta. IHttpContextAccessor.HttpContext non deve essere archiviato in un campo o in una variabile.

Non eseguire questa operazione: l'esempio seguente archivia il/la HttpContext in un campo e quindi tenta di usarlo in un secondo momento.

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

Il codice precedente acquisisce spesso un valore Null o non corretto HttpContext nel costruttore.

Eseguire questa operazione: L'esempio seguente:

  • Archivia IHttpContextAccessor in un campo.
  • Usa il HttpContext campo al momento corretto e verifica la presenza di 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");
        }
    }
}

Non accedere a HttpContext da più thread

HttpContext non è thread-safe. L'accesso a HttpContext da più thread in parallelo può comportare comportamenti imprevisti, ad esempio il server smette di rispondere, si verificano arresti anomali e si verifica il danneggiamento dei dati.

Non eseguire questa operazione: nell'esempio seguente vengono eseguite tre richieste parallele e viene registrato il percorso della richiesta in ingresso prima e dopo la richiesta HTTP in uscita. Il percorso della richiesta è accessibile da più thread, potenzialmente in parallelo.

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

Eseguire questa operazione: nell'esempio seguente vengono copiati tutti i dati dalla richiesta in ingresso prima di effettuare le tre richieste parallele.

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

Non usare HttpContext dopo il completamento della richiesta

HttpContext è valido solo se è presente una richiesta HTTP attiva nella pipeline ASP.NET Core. L'intera pipeline ASP.NET Core è una catena asincrona di delegati che esegue ogni richiesta. Quando il Task restituito da questa catena termina, HttpContext viene riciclato.

Non eseguire questa operazione: l'esempio seguente usa async void che rende completa la richiesta HTTP al raggiungimento del primo await :

  • L'uso di async void è sempre una procedura non valida nelle app ASP.NET Core.
  • Il codice di esempio accede all'elemento dopo HttpResponse una volta completata la richiesta HTTP.
  • L'accesso tardivo arresta in modo anomalo il processo.
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");
    }
}

Eseguire questa operazione: l'esempio seguente restituisce un Task al framework, quindi la richiesta HTTP non viene completata fino al completamento dell'azione.

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

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

Non acquisire HttpContext nei thread in background

Non eseguire questa operazione: L'esempio seguente mostra che una chiusura acquisisce HttpContext dalla proprietà Controller. Si tratta di una procedura non valida perché l'elemento di lavoro potrebbe:

  • Eseguire al di fuori dell'ambito della richiesta.
  • Tentativo di leggere quello sbagliato 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();
}

Eseguire questa operazione: L'esempio seguente:

  • Copia i dati necessari nell'attività in background durante l'elaborazione della richiesta.
  • Non fa riferimento a nulla del controller.
[HttpGet("/fire-and-forget-3")]
public IActionResult GoodFireAndForget()
{
    string path = HttpContext.Request.Path;
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        Log(path);
    });

    return Accepted();
}

Le attività in background devono essere implementate come servizi ospitati. Per altre informazioni, vedere Attività in background con servizi ospitati.

Non acquisire i servizi iniettati nei controller nei thread in background

Non eseguire questa operazione: l'esempio seguente mostra una chiusura che acquisisce l'oggetto DbContext dal Controller parametro action. Questa è una cattiva pratica. L'elemento di lavoro può essere eseguito all'esterno dell'ambito della richiesta. L'oggetto ContosoDbContext è associato alla richiesta, generando un oggetto 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();
}

Eseguire questa operazione: L'esempio seguente:

  • Inserisce un IServiceScopeFactory per creare un ambito nell'elemento di lavoro in background. IServiceScopeFactory è un singleton.
  • Crea un nuovo ambito di dependency injection nel thread in background.
  • Non fa riferimento a nulla del controller.
  • Non acquisisce l'oggetto ContosoDbContext dalla richiesta in ingresso.
[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();
}

Il codice evidenziato seguente:

  • Crea un ambito per la durata dell'operazione in background e risolve i servizi da esso.
  • Utilizza ContosoDbContext dall'ambito corretto.
[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();
}

Non modificare il codice di stato o le intestazioni dopo l'avvio del corpo della risposta

ASP.NET Core non memorizza nel buffer il corpo della risposta HTTP. La prima volta che viene scritta la risposta:

  • Le intestazioni vengono inviate insieme a quel blocco del corpo al client.
  • Non è più possibile modificare le intestazioni di risposta.

Non eseguire questa operazione: il codice seguente tenta di aggiungere intestazioni di risposta dopo l'avvio della risposta:

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

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

Nel codice precedente, context.Response.Headers["test"] = "test value"; genererà un'eccezione se next() ha scritto nella risposta.

Eseguire questa operazione: l'esempio seguente controlla se la risposta HTTP è stata avviata prima di modificare le intestazioni.

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

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

Eseguire questa operazione: l'esempio seguente usa HttpResponse.OnStarting per impostare le intestazioni prima che le intestazioni di risposta vengano inviate al client.

Controllare se la risposta non è stata avviata consente di registrare un callback che verrà richiamato subito prima della scrittura delle intestazioni di risposta. Verifica se la risposta non è ancora iniziata:

  • Consente di aggiungere o eseguire l'override delle intestazioni just-in-time.
  • Non richiede la conoscenza del middleware successivo nella pipeline.
app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        context.Response.Headers["someheader"] = "somevalue";
        return Task.CompletedTask;
    });

    await next();
});

Non chiamare next() se si è già iniziato a scrivere nel corpo della risposta

I componenti si aspettano di essere chiamati solo se per loro è possibile gestire e manipolare la risposta.

Usare l'hosting in-process con IIS

Se si usa l'hosting in-process, un'app ASP.NET Core viene eseguita nello stesso processo del processo di lavoro IIS. L'hosting in-process offre prestazioni migliorate rispetto all'hosting out-of-process perché le richieste non vengono trasmesse tramite proxy sull'adapter di loopback. La scheda di loopback è un'interfaccia di rete che restituisce il traffico di rete in uscita allo stesso computer. IIS gestisce la gestione dei processi tramite il Windows Process Activation Service (WAS).

Per impostazione predefinita, i progetti utilizzano il modello di hosting in-process in ASP.NET Core 3.0 o versioni successive.

Per altre informazioni, vedere Host ASP.NET Core in Windows con IIS

Non presumere che HttpRequest.ContentLength sia diverso da null

HttpRequest.ContentLength è Null se l'intestazione Content-Length non viene ricevuta. Null in questo caso indica che la lunghezza del corpo della richiesta non è nota; non significa che la lunghezza sia zero. Poiché tutti i confronti con null (ad eccezione di ==) restituiscono false, il confronto Request.ContentLength > 1024, ad esempio, potrebbe restituire false quando la dimensione del corpo della richiesta è superiore a 1024. Non sapere questo può causare problemi di sicurezza nelle app. Si potrebbe pensare di essere protetti da richieste troppo grandi quando in realtà non lo si è.

Per altre informazioni, vedere questa risposta StackOverflow.

Modelli di app Web aziendali

Per indicazioni sulla creazione di un'app Web affidabile, sicura, efficiente, testabile e scalabile ASP.NET Core, vedere modelli di app Web Enterprise. È disponibile un'app Web di esempio completa di qualità di produzione che implementa i modelli.