Práticas recomendadas do ASP.NET Core

Por Mike Rousos

Este artigo fornece diretrizes para maximizar o desempenho e a confiabilidade dos aplicativos ASP.NET Core.

Armazenar em cache agressivamente

O cache é discutido em várias partes deste artigo. Para obter mais informações, confira Visão geral do armazenamento em cache no ASP.NET Core.

Entender caminhos do código dinâmicos

Neste artigo, um caminho do código dinâmico é definido como um caminho do código que é frequentemente chamado e onde grande parte do tempo de execução ocorre. Os caminhos do código dinâmicos normalmente limitam a expansão e o desempenho do aplicativo e são discutidos em várias partes deste artigo.

Evitar chamadas de bloqueio

Os aplicativos ASP.NET Core devem ser projetados para processar várias solicitações simultaneamente. As APIs assíncronas permitem que um pequeno pool de threads cuide de milhares de solicitações simultâneas sem aguardar chamadas de bloqueio. Em vez de aguardar a conclusão de uma tarefa síncrona de execução longa, o thread pode trabalhar em outra solicitação.

Um problema de desempenho comum em aplicativos ASP.NET Core é o bloqueio de chamadas que poderiam ser assíncronas. Muitas chamadas de bloqueio síncronas levam à Privação do pool de threads e a tempos de resposta degradados.

Não bloqueie a execução assíncrona chamando Task.Wait ou Task<TResult>.Result. Não adquira bloqueios em caminhos do código comuns. Os aplicativos ASP.NET Core têm o melhor desempenho quando arquitetados para executar código em paralelo. Não chame Task.Run e aguarde imediatamente. O ASP.NET Core já executa o código do aplicativo em threads de Pool de Threads normais, portanto, chamar Task.Run resulta apenas no agendamento extra desnecessário do Pool de Threads. Mesmo que o código agendado bloqueie um thread, Task.Run não impede isso.

  • Torne os caminhos do código dinâmicos assíncronos.
  • Chame as APIs de operações de E/S, acesso a dados e execução longa de forma assíncrona se uma API assíncrona estiver disponível.
  • Não use Task.Run para tornar uma API síncrona assíncrona.
  • Torne as ações de controlador/Razor Page assíncronas. Toda a pilha de chamadas é assíncrona para se beneficiar de padrões async/await.

Um criador de perfil, como PerfView, pode ser usado para localizar threads adicionados frequentemente ao Pool de threads. O evento Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start indica um thread adicionado ao pool de threads.

Retornar grandes coleções em várias páginas menores

Uma página da Web não deve carregar grandes quantidades de dados de uma só vez. Ao retornar uma coleção de objetos, considere se ela pode levar a problemas de desempenho. Determine se o design pode produzir os seguintes resultados ruins:

Adicione paginação para atenuar os cenários anteriores. Usando parâmetros de índice e tamanho de página, os desenvolvedores devem privilegiar o design de retorno de um resultado parcial. Quando um resultado exaustivo for exigido, a paginação deverá ser usada para preencher de forma assíncrona lotes de resultados a fim de evitar o bloqueio de recursos do servidor.

Para obter mais informações sobre paginação e limitação do número de registros retornados, confira:

Retorne IEnumerable<T> ou IAsyncEnumerable<T>

O retorno de IEnumerable<T> de uma ação resulta na iteração da coleção síncrona pelo serializador. O resultado é o bloqueio de chamadas e um potencial de privação do pool de threads. Para evitar enumeração síncrona, use ToListAsync antes de retornar o enumerável.

A partir do ASP.NET Core 3.0, IAsyncEnumerable<T> pode ser usado como uma alternativa para IEnumerable<T> que enumera de forma assíncrona. Para obter mais informações, confira Tipos de retorno de ação do controlador.

Minimizar alocações de objetos grandes

O coletor de lixo do .NET Core gerencia a alocação e a liberação de memória automaticamente em aplicativos do ASP.NET Core. A coleta automática de lixo geralmente significa que os desenvolvedores não precisam se preocupar com como ou quando a memória é liberada. No entanto, a limpeza de objetos não referenciados exige tempo de CPU, ou seja, os desenvolvedores devem minimizar a alocação de objetos em caminhos do código dinâmicos. A coleta de lixo é especialmente cara para objetos grandes (> 85.000 Kb). Objetos grandes são armazenados no heap de objetos grandes e exigem uma coleta de lixo completa (geração 2) para serem limpos. Ao contrário das coleções de geração 0 e de geração 1, uma coleção de geração 2 requer uma suspensão temporária da execução do aplicativo. A alocação e a desalocação frequentes de objetos grandes podem causar um desempenho inconsistente.

Recomendações:

  • Considere armazenar em cache objetos grandes que sejam usados com frequência. O armazenamento de objetos grandes em cache evita alocações caras.
  • Faça buffers de pool usando um ArrayPool<T> para armazenar grandes matrizes.
  • Não aloque muitos objetos grandes de curta duração em caminhos do código dinâmicos.

Problemas de memória, como o anterior, podem ser diagnosticados com o exame das estatísticas de GC (coleta de lixo) no PerfView e o exame de:

  • Tempo de pausa da coleta de lixo.
  • A porcentagem de tempo do processador gasta na coleta de lixo.
  • A quantidade de coletas de lixo que são de geração 0, 1 e 2.

Para obter mais informações, confira Coleta de Lixo e Desempenho.

Otimizar o acesso a dados e E/S

As interações com um armazenamento de dados e outros serviços remotos geralmente são as partes mais lentas de um aplicativo ASP.NET Core. Ler e gravar dados com eficiência é fundamental para um bom desempenho.

Recomendações:

  • Chame todas as APIs de acesso a dados de forma assíncrona.
  • Não recupere mais dados do que o necessário. Escreva consultas a fim de retornar apenas os dados necessários para a solicitação HTTP atual.
  • Considere armazenar em cache dados acessados com frequência recuperados de um banco de dados ou serviço remoto se dados ligeiramente desatualizados forem aceitáveis. Dependendo do cenário, use um MemoryCache ou um DistributedCache. Para obter mais informações, confira Cache de resposta no ASP.NET Core.
  • Minimize as viagens de ida e volta da rede. A meta é recuperar os dados necessários em uma única chamada em vez de em várias chamadas.
  • Useconsultas sem acompanhamento no Entity Framework Core ao acessar dados para finalidades somente leitura. O EF Core pode retornar os resultados de consultas sem acompanhamento com mais eficiência.
  • Filtre e agregue consultas LINQ (com instruções .Where, .Selectou .Sum, por exemplo) para que a filtragem seja executada pelo banco de dados.
  • Considere que EF Core resolve alguns operadores de consulta no cliente, o que pode levar a uma execução de consulta ineficiente. Para obter mais informações, confira Problemas de desempenho de avaliação do cliente.
  • Não use consultas de projeção em coleções, o que pode resultar na execução de consultas SQL "N + 1". Para obter mais informações, confira Otimização de subconsultas correlacionadas.

As abordagens a seguir podem melhorar o desempenho em aplicativos de grande escala:

Recomendamos medir o impacto das abordagens de alto desempenho anteriores antes de confirmar a base de código. A complexidade adicional das consultas compiladas pode não justificar a melhoria de desempenho.

Os problemas de consulta podem ser detectados com o exame do tempo gasto acessando dados com o Application Insights ou com ferramentas de criação de perfil. A maioria dos bancos de dados também disponibiliza estatísticas sobre consultas executadas com frequência.

Conexões HTTP do pool com HttpClientFactory

Embora HttpClient implemente a interface IDisposable, ela foi projetada para reutilização. Instâncias HttpClient fechadas deixam soquetes abertos no estado TIME_WAIT por um curto período de tempo. Se um caminho do código que cria e descarta objetos HttpClient é usado com frequência, o aplicativo pode esgotar os soquetes disponíveis. HttpClientFactory foi apresentado no ASP.NET Core 2.1 como uma solução para esse problema. Ele manipula o pool de conexões HTTP para otimizar o desempenho e a confiabilidade. Para obter mais informações, confira Usar HttpClientFactory para implementar solicitações HTTP resilientes.

Recomendações:

Manter caminhos de código comuns rápidos

Você quer que todo o código seja rápido. Os caminhos do código chamados com frequência são os que mais devem ser otimizados. Estão incluídos:

  • Componentes de middleware no pipeline de processamento de solicitações do aplicativo, especialmente middleware executado no início do pipeline. Esses componentes têm um grande impacto no desempenho.
  • Código executado para cada solicitação ou várias vezes por solicitação. Por exemplo, registro em log personalizado, manipuladores de autorização ou inicialização de serviços transitórios.

Recomendações:

Concluir tarefas de longa execução fora das solicitações HTTP

A maioria das solicitações para um aplicativo ASP.NET Core pode ser manipulada por um controlador ou modelo de página que chama os serviços necessários e retorna uma resposta HTTP. Para algumas solicitações que envolvem tarefas de execução longa, é melhor tornar todo o processo de solicitação-resposta assíncrono.

Recomendações:

  • Não aguarde a conclusão de tarefas de execução prolongada como parte do processamento de solicitação HTTP comum.
  • Considere lidar com solicitações de longa execução com serviços em segundo plano ou fora de processo com uma Azure Function. A conclusão do trabalho fora do processo é especialmente benéfica em caso de tarefas com uso intensivo de CPU.
  • Use opções de comunicação em tempo real, como SignalR, para se comunicar com clientes de forma assíncrona.

Minificar ativos do cliente

Os aplicativos ASP.NET Core com front-ends complexos normalmente atendem a muitos arquivos JavaScript, CSS ou de imagem. O desempenho das solicitações de carga iniciais pode ser aprimorado:

  • Agrupamento, que combina vários arquivos em um.
  • Minificação, o que reduz o tamanho dos arquivos removendo o espaço em branco e os comentários.

Recomendações:

  • Use as diretrizes de agrupamento e minificação, que mencionam ferramentas compatíveis e mostram como usar a tag environment do ASP.NET Core para lidar com ambientes de Production e de Development.
  • Considere outras ferramentas de terceiros, como o Webpack, para um gerenciamento complexo de ativos do cliente.

Compactar respostas

A redução do tamanho da resposta geralmente aumenta a capacidade de resposta de um aplicativo, muitas vezes dramaticamente. Uma maneira de reduzir os tamanhos de carga é compactar as respostas de um aplicativo. Para obter mais informações, confira Compactação de resposta.

Usar a versão mais recente do ASP.NET Core

Cada nova versão do ASP.NET Core inclui aprimoramentos de desempenho. Otimizações no .NET Core e no ASP.NET Core significam que as versões mais recentes geralmente superam as versões mais antigas. Por exemplo, o .NET Core 2.1 adicionou suporte a expressões regulares compiladas e se beneficiou de Span<T>. O ASP.NET Core 2.2 adicionou suporte ao HTTP/2. O ASP.NET Core 3.0 adiciona muitos aprimoramentos que reduzem o uso de memória e melhoram a taxa de transferência. Se o desempenho for uma prioridade, considere atualizar para a versão atual do ASP.NET Core.

Minimizar exceções

As exceções devem ser raras. O lançamento e a captura de exceções são lentos em relação a outros padrões de fluxo de código. Por isso, as exceções não devem ser usadas para controlar o fluxo normal do programa.

Recomendações:

  • Não use exceções de lançamento ou captura como um meio de fluxo de programa normal, especialmente em caminhos de código dinâmicos.
  • Inclua lógica no aplicativo para detectar e manipular condições que causariam uma exceção.
  • Lance ou capture exceções em caso de condições incomuns ou inesperadas.

Ferramentas de diagnóstico de aplicativo, como o Application Insights, podem ajudar a identificar exceções comuns em um aplicativo que podem afetar o desempenho.

Evitar leitura ou gravação síncrona no corpo HttpRequest/HttpResponse

Toda a E/S no ASP.NET Core é assíncrona. Os servidores implementam a interface Stream, que tem sobrecargas síncronas e assíncronas. As assíncronas devem ser preferidas para evitar o bloqueio de threads do pool de threads. O bloqueio de threads pode levar à privação do pool de threads.

Não faça o seguinte: O exemplo a seguir usa o ReadToEnd. Ele bloqueia o thread atual para aguardar o resultado. Este é um exemplo de síncrono em vez de assíncrono.

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

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

No código anterior, Get lê de forma síncrona todo o corpo da solicitação HTTP na memória. Se o cliente está carregando lentamente, o aplicativo está usando síncrono em vez de assíncrono. O aplicativo usa síncrono em vez de assíncrono porque o KestrelNÃO dá suporte a leituras síncronas.

Faça o seguinte: O exemplo a seguir usa ReadToEndAsync e não bloqueia o thread durante a leitura.

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

}

O código anterior lê de forma síncrona todo o corpo da solicitação HTTP na memória.

Aviso

Se a solicitação for grande, a leitura de todo o corpo da solicitação HTTP na memória poderá levar a uma condição de OOM (memória insuficiente). O OOM pode resultar em uma negação de serviço. Para obter mais informações, confira Evitar ler corpos de solicitação grandes ou corpos de resposta na memória neste artigo.

Faça o seguinte: O exemplo abaixo é totalmente assíncrono e usa um corpo de solicitação sem buffer:

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

O código anterior desserializa de forma assíncrona o corpo da solicitação em um objeto C#.

Preferir ReadFormAsync em vez de Request.Form

Use HttpContext.Request.ReadFormAsync em vez de HttpContext.Request.Form. HttpContext.Request.Form pode ser lido com segurança apenas nas seguintes condições:

  • O formulário foi lido por uma chamada para ReadFormAsync e
  • O valor do formulário armazenado em cache está sendo lido usando HttpContext.Request.Form

Não faça o seguinte: O exemplo a seguir usa HttpContext.Request.Form. HttpContext.Request.Form usa síncrono em vez de assíncrono e pode levar à privação do 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();
    }

Faça o seguinte: O exemplo a seguir usa HttpContext.Request.ReadFormAsync para ler o corpo do formulário de forma assíncrona.

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

Evite ler corpos de solicitação grandes ou corpos de resposta na memória

No .NET, cada alocação de objeto maior ou igual a 85.000 bytes acaba no large object heap (LOH). Objetos grandes são caros por dois motivos:

  • O custo de alocação é alto porque a memória de um objeto grande recém-alocado precisa ser limpa. O CLR garante que a memória de todos os objetos recém-alocados seja limpa.
  • O LOH é coletado com o restante do heap. O LOH requer uma coleta de lixo completa ou coleta Gen2.

Esta postagem no blog descreve o problema de forma sucinta:

Quando um objeto grande é alocado, ele é marcado como objeto Gen 2. Não Gen 0 como para objetos pequenos. As consequências são que, se você ficar sem memória no LOH, o GC limpará todo o heap gerenciado, não apenas o LOH. Assim, ele limpa Gen 0, Gen 1 e Gen 2, incluindo o LOH. Isso é chamado de coleta de lixo completa e é a coleta de lixo mais demorada. Para muitos aplicativos, pode ser aceitável. Mas definitivamente não para servidores Web de alto desempenho, em que poucos buffers de memória grandes são necessários para lidar com uma solicitação da Web média (ler de um soquete, descompactar, decodificar JSON e muito mais).

O armazenamento de um corpo de solicitação ou resposta grande em um único byte[] ou string:

  • Pode resultar no exaurimento rápido do espaço no LOH.
  • Pode causar problemas de desempenho para o aplicativo devido a GCs completos em execução.

Trabalhando com uma API de processamento de dados síncrona

Ao usar um serializador/desserializador que só dá suporte a leituras e gravações síncronas (por exemplo, Json.NET):

  • Armazene os dados na memória de forma assíncrona antes de transmiti-los ao serializador/desserializador.

Aviso

Se a solicitação for grande, ela poderá levar a uma condição de OOM (memória insuficiente). O OOM pode resultar em uma negação de serviço. Para obter mais informações, confira Evitar ler corpos de solicitação grandes ou corpos de resposta na memória neste artigo.

O ASP.NET Core 3.0 usa System.Text.Json por padrão para serialização JSON. System.Text.Json:

  • Lê e grava JSON de forma assíncrona.
  • É otimizado para texto UTF-8.
  • Normalmente ele tem desempenho mais alto que Newtonsoft.Json.

Não armazenar IHttpContextAccessor.HttpContext em um campo

O IHttpContextAccessor.HttpContext retorna oHttpContext da solicitação ativa quando acessado no thread de solicitação. O IHttpContextAccessor.HttpContextnão deve ser armazenado em um campo ou variável.

Não faça o seguinte: O exemplo a seguir armazena HttpContext um campo e tenta usá-lo mais tarde.

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

O código anterior captura frequentemente um HttpContext nulo ou incorreto no construtor.

Faça o seguinte: Este exemplo:

  • Armazena o IHttpContextAccessor em um campo.
  • Usa o campo HttpContext no momento correto e verifica 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ão acessar HttpContext de vários threads

HttpContext não é seguros para threads. O acesso a HttpContext de vários threads em paralelo pode resultar em comportamento inesperado, como travamentos, falhas e corrupção de dados.

Não faça o seguinte: O exemplo a seguir faz três solicitações paralelas e registra o caminho de solicitação de entrada antes e depois da solicitação HTTP de saída. O caminho da solicitação é acessado de vários threads, possivelmente em paralelo.

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

Faça o seguinte: O exemplo a seguir copia todos os dados da solicitação de entrada antes de fazer as três solicitações paralelas.

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

Não usar o HttpContext após a conclusão da solicitação

HttpContext só é válido desde que haja uma solicitação HTTP ativa no pipeline do ASP.NET Core. Todo o pipeline do ASP.NET Core é uma cadeia assíncrona de delegados que executa todas as solicitações. Quando o Task retornado dessa cadeia é concluído, o HttpContext é reciclado.

Não faça o seguinte: O exemplo abaixo usa async void, o que faz com que a solicitação HTTP seja concluída quando o primeiro await é atingido:

  • O uso de async void é SEMPRE uma prática incorreta em aplicativos do ASP.NET Core.
  • O código de exemplo acessa o HttpResponse após a conclusão da solicitação HTTP.
  • O acesso atrasado trava o 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");
    }
}

Faça o seguinte: O exemplo a seguir retorna um Task para a estrutura, ou seja, a solicitação HTTP não é concluída até que a ação seja concluída.

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

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

Não capturar o HttpContext em threads em segundo plano

Não faça o seguinte: O exemplo abaixo mostra que um fechamento está capturando o HttpContext da propriedade Controller. Essa é uma prática incorreta porque o item de trabalho pode:

  • Ser executado fora do escopo da solicitação.
  • Tentar ler o erro 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();
}

Faça o seguinte: Este exemplo:

  • Copia os dados obrigatórios na tarefa em segundo plano durante a solicitação.
  • Não faz referência a nada do controlador.
[HttpGet("/fire-and-forget-3")]
public IActionResult GoodFireAndForget()
{
    string path = HttpContext.Request.Path;
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        Log(path);
    });

    return Accepted();
}

As tarefas em segundo plano devem ser implementadas como serviços hospedados. Saiba mais em Tarefas em segundo plano com serviços hospedados.

Não capturar serviços injetados nos controladores em threads em segundo plano

Não faça o seguinte: O exemplo a seguir mostra um fechamento que está capturando o DbContext do parâmetro de ação Controller. Essa é uma má prática. O item de trabalho pode ser executado fora do escopo da solicitação. O ContosoDbContext tem a solicitação como escopo, resultando em uma 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();
}

Faça o seguinte: Este exemplo:

  • Injeta um IServiceScopeFactory para criar um escopo no item de trabalho em segundo plano. IServiceScopeFactory é um singleton.
  • Cria outro escopo de injeção de dependência no thread em segundo plano.
  • Não faz referência a nada do controlador.
  • Não captura o ContosoDbContext da solicitação de entrada.
[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();
}

O seguinte código realçado:

  • Cria um escopo para o tempo de vida da operação em segundo plano e resolve os serviços dela.
  • Usa ContosoDbContext do escopo correto.
[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();
}

Não modificar o código de status ou os cabeçalhos depois que o corpo da resposta for iniciado

O ASP.NET Core não armazena em buffer o corpo da resposta HTTP. A primeira vez que a resposta é gravada:

  • Os cabeçalhos são enviados junto com essa parte do corpo ao cliente.
  • Não é mais possível alterar cabeçalhos de resposta.

Não faça o seguinte: O código abaixo tenta adicionar cabeçalhos de resposta após a resposta já ter sido iniciada:

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

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

No código anterior, context.Response.Headers["test"] = "test value"; lançará uma exceção se next() tiver gravado a resposta.

Faça o seguinte: O exemplo abaixo verifica se a resposta HTTP foi iniciada antes de modificar os cabeçalhos.

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

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

Faça o seguinte: O exemplo abaixo usa HttpResponse.OnStarting para definir os cabeçalhos antes que os cabeçalhos de resposta sejam liberados para o cliente.

Verificar se a resposta não foi iniciada permite registrar um retorno de chamada que será invocado logo antes de os cabeçalhos de resposta serem gravados. Verificar se a resposta não foi iniciada:

  • Dá a capacidade de acrescentar ou substituir cabeçalhos em cima da hora.
  • Não requer conhecimento do próximo middleware no pipeline.
app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        context.Response.Headers["someheader"] = "somevalue";
        return Task.CompletedTask;
    });

    await next();
});

Não chamar next() se você já tiver começado a gravar no corpo da resposta

Os componentes só esperam ser chamados se for possível manipular a resposta.

Usar a hospedagem em processo com o IIS

Usando uma hospedagem em processo, um aplicativo ASP.NET Core é executado no mesmo processo que seu processo de trabalho do IIS. A hospedagem no processo fornece melhor desempenho em relação à hospedagem fora do processo porque as solicitações não são feitas por proxy no adaptador de loopback. O adaptador de loopback é um adaptador de rede que devolve o tráfego de rede de saída para o mesmo computador. O IIS manipula o gerenciamento de processos com o WAS (Serviço de Ativação de Processos do Windows).

Os projetos são padrão para o modelo de hospedagem no processo no ASP.NET Core 3.0 e posterior.

Para obter mais informações, confira Hospedar o ASP.NET Core no Windows com o IIS

Não supor que HttpRequest.ContentLength não seja nulo

HttpRequest.ContentLength será nulo se o cabeçalho Content-Length não for recebido. Nulo, nesse caso, significa que o tamanho do corpo da solicitação não é conhecido; isso não significa que o tamanho seja zero. Como todas as comparações com nulo (exceto ==) retornam false, o Request.ContentLength > 1024 de comparação, por exemplo, poderá retornar false quando o tamanho do corpo da solicitação for maior que 1024. Não saber isso pode levar a falhas de segurança em aplicativos. Você pode pensar que está se protegendo contra solicitações muito grandes quando não está.

Para obter mais informações, confira esta resposta do StackOverflow.

Padrões de aplicativos Web confiáveis

Veja O padrão de aplicativo da Web confiável para.NET vídeos do YouTube e artigo para obter orientação sobre como criar um aplicativo ASP.NET Core moderno, confiável, de alto desempenho, testável, econômico e escalonável, seja do zero ou refatorando um existente aplicativo.