Procedure consigliate di base di ASP.NET

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.

Informazioni sui percorsi di codice ad accesso frequente

In questo articolo un percorso di codice frequente 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 di bloccare le chiamate

ASP.NET le app 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 di blocco. Anziché attendere il completamento di un'attività sincrona a esecuzione prolungata, il thread può funzionare su un'altra richiesta.

Un problema di prestazioni comune nelle app di base di ASP.NET blocca le chiamate che potrebbero essere asincrone. Molte chiamate di blocco sincrono portano alla fame del pool di thread e ai tempi di risposta degradati.

Non bloccare l'esecuzione asincrona chiamando Task.Wait o Task<TResult>.Result. Non acquisire blocchi in percorsi di codice comuni. ASP.NET le app Core funzionano meglio quando è progettato per eseguire il codice in parallelo. Non chiamare Task.Run e attendere 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 necessario. Anche se il codice pianificato blocca un thread, Task.Run non ne impedisce.

  • Rendere asincroni i percorsi di codice ad accesso frequente.
  • 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.
  • Rendere asincrone le azioni controller/Razor pagina. L'intero stack di chiamate è asincrono per trarre vantaggio dai modelli async/await .

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 un thread aggiunto al pool di thread.

Restituire 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. Determinare se la progettazione potrebbe produrre i risultati scarsi seguenti:

Aggiungere la paginazione per attenuare gli scenari precedenti. Usando i parametri delle dimensioni della pagina e dell'indice di pagina, gli sviluppatori devono favorire la progettazione della restituzione di un risultato parziale. Quando è necessario un risultato completo, 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 un potenziale per la fame del pool di thread. Per evitare l'enumerazione sincrona, usare ToListAsync prima di restituire l'enumerabile.

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

Ridurre al minimo le allocazioni di oggetti di grandi dimensioni

. NET Core 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, in modo che gli sviluppatori debbano 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.

Raccomandazioni:

  • Prendere 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.
  • Eseguire i buffer del pool usando un ArrayPool<T> oggetto per archiviare matrici di grandi dimensioni.
  • Non allocare molti oggetti di grandi dimensioni di breve durata nei percorsi di codice ad accesso frequente.

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

  • Tempo di sospensione di Garbage Collection.
  • Percentuale del tempo del processore impiegato in 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.

Raccomandazioni:

  • 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.
  • Prendere in considerazione la memorizzazione nella cache dei dati a cui si accede di frequente recuperati da un database o da un servizio remoto se i dati 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.
  • Ridurre al minimo i round trip di rete. L'obiettivo è recuperare i dati necessari in una singola chiamata anziché più chiamate.
  • Usare query senza rilevamento in Entity Framework Core per l'accesso ai dati a scopo di sola lettura. EF Core può restituire i risultati delle query senza rilevamento in modo più efficiente.
  • Filtrare e aggregare query LINQ (ad esempio con .Whereistruzioni , .Selecto .Sum ) in modo che il filtro venga eseguito dal database.
  • Tenere presente che EF Core risolve alcuni operatori di query nel client, che possono causare un'esecuzione di query inefficiente. Per altre informazioni, vedere Problemi di prestazioni di 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.

Pool di 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.

Raccomandazioni:

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. tra cui:

  • I componenti middleware nella pipeline di elaborazione delle richieste dell'app, in particolare il middleware viene eseguito 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.

Raccomandazioni:

  • 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 ad accesso frequente.

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.

Raccomandazioni:

  • Non attendere il completamento delle attività a esecuzione prolungata come parte dell'elaborazione normale delle richieste HTTP.
  • Valutare la possibilità di gestire le richieste a esecuzione prolungata con servizi in background o out-of-process con una funzione di Azure. Il completamento del lavoro out-of-process è particolarmente utile per le attività a elevato utilizzo di CPU.
  • Usare opzioni di comunicazione in tempo reale, ad esempio SignalR, per comunicare con i client in modo asincrono.

Minify client assets

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

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

Raccomandazioni:

  • Usare le linee guida per la creazione di bundle e la minimizzazione, che indicano gli strumenti compatibili e illustrano come usare il tag di environment ASP.NET Core per gestire entrambi Development gli ambienti e Production .
  • Prendere in considerazione 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 Core e ASP.NET Core indicano che le versioni più recenti hanno in genere prestazioni migliori rispetto alle 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 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. La generazione e l'intercettazione di eccezioni sono lente rispetto ad altri modelli di flusso di codice. Per questo motivo, le eccezioni non devono essere usate per controllare il normale flusso del programma.

Raccomandazioni:

  • Non usare la generazione o l'intercettazione di eccezioni come mezzo del normale flusso di programma, in particolare nei percorsi di codice ad accesso frequente.
  • Includere la logica nell'app per rilevare e gestire le condizioni che causerebbero un'eccezione.
  • Generare o intercettare 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 può 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. I thread bloccanti possono causare la fame del pool di thread.

Non eseguire questa operazione: nell'esempio seguente viene utilizzato .ReadToEnd Blocca il thread corrente per attendere il risultato. Questo è un esempio di sincronizzazione su asincrono.

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 legge Get in modo sincrono l'intero corpo della richiesta HTTP in memoria. Se il client viene caricato lentamente, l'app esegue la sincronizzazione su asincrona. L'app esegue la sincronizzazione su asincrona perché Kestrel non 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.

Avviso

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 Evitare di leggere corpi di richiesta di grandi dimensioni o corpi di risposta in memoria 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#.

Preferisce ReadFormAsync su Request.Form

Usare HttpContext.Request.ReadFormAsync invece di HttpContext.Request.Form. HttpContext.Request.Form può essere letto in modo sicuro solo con le 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 eseguire questa operazione: nell'esempio seguente viene HttpContext.Request.Formusato . HttpContext.Request.Form usa la sincronizzazione su asincrona e può causare la fame 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 viene HttpContext.Request.ReadFormAsync 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 allocata deve essere cancellata. CLR garantisce che la memoria per tutti gli oggetti appena allocati venga cancellata.
  • LOH viene raccolto con il resto dell'heap. LOH richiede un'operazione completa di Garbage Collection o 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 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, incluso LOH. 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 (letta da un socket, decompressione, decodifica 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 di controller di dominio completi in esecuzione.

Uso di un'API di elaborazione dati sincrona

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.

Avviso

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 Evitare di leggere corpi di richiesta di grandi dimensioni o corpi di risposta in memoria in questo articolo.

ASP.NET Core 3.0 usa System.Text.Json per impostazione predefinita per JSla serializzazione ON. System.Text.Json:

  • Legge e scrive JSON in modo asincrono.
  • È ottimizzato per il testo UTF-8.
  • In genere sono 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. Non IHttpContextAccessor.HttpContext deve essere archiviato in un campo o in una variabile.

Non eseguire questa operazione: l'esempio seguente archivia in HttpContext 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 l'oggetto 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

HttpContextnon è thread-safe. L'accesso HttpContext da più thread in parallelo può comportare un comportamento imprevisto, ad esempio blocchi, arresti anomali e 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. Al termine dell'oggetto Task restituito da questa catena, viene HttpContext riciclato.

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

  • L'uso async void di è sempre una procedura non valida nelle app ASP.NET Core.
  • Il codice di esempio accede a HttpResponse dopo il completamento della richiesta HTTP.
  • L'accesso in ritardo 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 oggetto 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 l'oggetto HttpContextController dalla proprietà . Si tratta di una procedura non valida perché l'elemento di lavoro potrebbe:

  • Eseguire all'esterno dell'ambito della richiesta.
  • Tentare di leggere l'errore 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 la richiesta.
  • Non fa riferimento a nulla dal 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 inseriti 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 ha come ambito la 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 oggetto IServiceScopeFactory per creare un ambito nell'elemento di lavoro in background. IServiceScopeFactory è un singleton.
  • Crea un nuovo ambito di inserimento delle dipendenze nel thread in background.
  • Non fa riferimento a nulla dal 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.
  • ContosoDbContext Usa 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 genererà context.Response.Headers["test"] = "test value"; un'eccezione se next() è stata scritta 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 scaricate nel 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 è stata avviata:

  • 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 è possibile gestirli e manipolare la risposta.

Usare l'hosting in-process con IIS

Se si usa l'hosting in-process, un'app ASP.NET Core esegue lo 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. Per gestire il processo, IIS usa il servizio Attivazione processo Windows (WAS).

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

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

Non presupporre che HttpRequest.ContentLength non sia 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 sapendo questo può causare problemi di sicurezza nelle app. Si potrebbe pensare di proteggersi da richieste troppo grandi quando non lo si è.

Per altre informazioni, vedere questa risposta StackOverflow.

Modelli di app Web affidabili

Per indicazioni sulla creazione di un'app moderna, affidabile, affidabile, affidabile, testabile, conveniente e scalabile ASP.NET Core, indipendentemente dal fatto che si tratti di un'app esistente o di refactoring di un'app esistente, vedi Modello di app Web Reliable Web for.NETYouTube.