Procedimientos recomendados de ASP.NET Core

Por Mike Rousos

En este artículo se proporcionan instrucciones para maximizar el rendimiento y la confiabilidad de las aplicaciones de ASP.NET Core.

Almacenamiento en caché de forma activa

El almacenamiento en caché se describe en varias partes de este artículo. Para más información, consulte Información general sobre el almacenamiento en caché en ASP.NET Core.

Descripción de las rutas de acceso al código activas

En este artículo, una ruta de acceso al código activa se define como una ruta de acceso al código a la que se llama con frecuencia y donde se produce gran parte del tiempo de ejecución. Las rutas de acceso al código activas suelen limitar la escalabilidad horizontal y el rendimiento de las aplicaciones y se describen en varias partes de este artículo.

Evitar llamadas de bloqueo

Las aplicaciones de ASP.NET Core deben diseñarse para procesar muchas solicitudes simultáneamente. Las API asincrónicas permiten que un pequeño grupo de subprocesos controle miles de solicitudes simultáneas sin esperar a las llamadas de bloqueo. En lugar de esperar a que se complete una tarea sincrónica de larga duración, el subproceso puede trabajar en otra solicitud.

Un problema de rendimiento común en las aplicaciones de ASP.NET Core es que las llamadas de bloqueo podrían ser asincrónicas. Muchas llamadas de bloqueo sincrónicas conducen a la escasez del grupo de subprocesos y a tiempos de respuesta degradados.

No bloquee la ejecución asincrónica llamando a Task.Wait o Task<TResult>.Result. No adquiera bloqueos en rutas de acceso al código comunes. Las aplicaciones de ASP.NET Core funcionan mejor cuando se diseñan para ejecutar código en paralelo. No llame a Task.Run y espere inmediatamente. ASP.NET Core ya ejecuta código de aplicación en los subprocesos normales del grupo de subprocesos, por lo que la llamada a Task.Run solo da lugar a la programación adicional innecesaria del grupo de subprocesos. Aunque el código programado bloquee un subproceso, Task.Run no lo impide.

  • No convierta las rutas de acceso al código activas en asincrónicas.
  • Realice llamadas a las API de acceso a datos, E/S y operaciones de larga duración de forma asincrónica si hay disponible una API asincrónica.
  • No use Task.Run para convertir una API sincrónica en asincrónica.
  • Realice acciones asincrónicas de controlador o páginas de Razor. Toda la pila de llamadas es asincrónica para beneficiarse de los patrones async/await.

Un generador de perfiles, como PerfView, se puede usar para buscar subprocesos que se agregan con frecuencia al grupo de subprocesos. El evento Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start indica que se agregó un subproceso al grupo de subprocesos.

Devolución de colecciones grandes en varias páginas más pequeñas

Una página web no debe cargar grandes cantidades de datos a la vez. Al devolver una colección de objetos, tenga en cuenta si podría provocar problemas de rendimiento. Determine si el diseño podría producir los siguientes resultados deficientes:

  • La excepción OutOfMemoryException o un consumo de memoria alto
  • Colapso del grupo de subprocesos (consulte los comentarios siguientes sobre IAsyncEnumerable<T>)
  • Tiempos de respuesta lentos
  • Recolección frecuente de elementos no utilizados

Agregue paginación para mitigar los escenarios anteriores. Con el uso de parámetros de índice de página y tamaño de página, los desarrolladores deben favorecer el diseño de devolución de un resultado parcial. Cuando se requiere un resultado exhaustivo, la paginación se debe usar para rellenar de forma asincrónica lotes de resultados a fin de evitar el bloqueo de los recursos del servidor.

Para obtener más información sobre la paginación y la limitación del número de registros devueltos, consulte:

Devolución de IEnumerable<T> o IAsyncEnumerable<T>

La devolución de IEnumerable<T> en una acción da como resultado la iteración de la colección sincrónica por parte del serializador. El resultado es el bloqueo de llamadas y una posibilidad de colapso del grupo de subprocesos. Para evitar la enumeración sincrónica, use ToListAsync antes de devolver el enumerable.

A partir de ASP.NET Core 3.0, IAsyncEnumerable<T> se puede usar como alternativa a IEnumerable<T>, que enumera de forma asincrónica. Para obtener más información, consulte Tipos de valor devuelto de acción del controlador.

Minimizar asignaciones de objetos grandes

El recolector de elementos no utilizados de .NET Core administra la asignación y liberación de memoria automáticamente en las aplicaciones de ASP.NET Core. Con la recolección automática de elementos no utilizados, normalmente los desarrolladores no necesitan preocuparse por cómo o cuándo se libera memoria. Sin embargo, la limpieza de objetos sin referencia tarda tiempo en la CPU, por lo que los desarrolladores deben minimizar la asignación de objetos en rutas de acceso al código activas. La recolección de elementos no utilizados es especialmente costosa en objetos grandes (>= 85.000 bytes). Los objetos grandes se almacenan en el montón de objetos grandes y requieren una recolección de elementos no utilizados completa (generación 2) para la limpieza. A diferencia de las colecciones de generación 0 y generación 1, una colección de generación 2 requiere una suspensión temporal de la ejecución de la aplicación. La asignación y desasignación frecuentes de objetos grandes puede provocar un rendimiento incoherente.

Recomendaciones:

  • Considere la posibilidad de almacenar en caché objetos grandes que se usan con frecuencia. El almacenamiento en caché de objetos grandes evita asignaciones costosas.
  • Cree búferes de grupo mediante ArrayPool<T> para almacenar matrices grandes.
  • No asigne muchos objetos grandes de corta duración en rutas de acceso al código activas.

Los problemas de memoria, como los anteriores, se pueden diagnosticar revisando las estadísticas de recolección de elementos no utilizados (GC) en PerfView y examinando lo siguiente:

  • Tiempo de pausa en la recolección de elementos no utilizados.
  • Porcentaje de tiempo del procesador dedicado a la recolección de elementos no utilizados.
  • Cuántas recolecciones de elementos no utilizados son de generación 0, 1 y 2.

Para obtener más información, consulte Recolección de elementos no utilizados y rendimiento.

Optimización del acceso a datos y E/S

Las interacciones con un almacén de datos y otros servicios remotos suelen ser las partes más lentas de una aplicación de ASP.NET Core. Leer y escribir datos de forma eficaz es fundamental para un buen rendimiento.

Recomendaciones:

  • Llame a todas las API de acceso a datos de forma asincrónica.
  • No recupere más datos de los necesarios. Escriba consultas para devolver solo los datos necesarios para la solicitud HTTP actual.
  • Considere la posibilidad de almacenar en caché los datos a los que se accede con frecuencia desde una base de datos o un servicio remoto si se aceptan datos ligeramente obsoletos. En función del escenario, use MemoryCache o DistributedCache. Para más información, consulte Almacenamiento en caché de respuestas en ASP.NET Core.
  • Minimice los recorridos de ida y vuelta de la red. El objetivo es recuperar los datos necesarios en una sola llamada en lugar de varias llamadas.
  • Useconsultas sin seguimiento en Entity Framework Core al acceder a los datos con fines de solo lectura. EF Core puede devolver los resultados de las consultas sin seguimiento de forma más eficaz.
  • Filtre y agregue consultas LINQ (por ejemplo, con instrucciones .Where, .Select o .Sum) para que la base de datos realice el filtrado.
  • Tenga en cuenta que EF Core resuelve algunos operadores de consulta en el cliente, lo que puede provocar una ejecución de consultas ineficaz. Para obtener más información, consulte Problemas de rendimiento de evaluación de cliente.
  • No use consultas de proyección en colecciones, lo que puede provocar la ejecución de consultas SQL "N + 1". Para obtener más información, consulte Optimización de subconsultas correlacionadas.

Los enfoques siguientes pueden mejorar el rendimiento en aplicaciones a gran escala:

Se recomienda medir el impacto de los enfoques anteriores de alto rendimiento antes de confirmar la base de código. Puede que la complejidad adicional de las consultas compiladas no justifique la mejora del rendimiento.

Los problemas de consulta se pueden detectar revisando el tiempo dedicado a acceder a los datos con Application Insights o con herramientas de generación de perfiles. La mayoría de las bases de datos también hacen que las estadísticas estén disponibles para las consultas ejecutadas con frecuencia.

Agrupación de conexiones HTTP con HttpClientFactory

Aunque HttpClient implementa la interfaz IDisposable, está diseñada para su reutilización. Las instancias cerradas de HttpClient dejan abiertos los sockets en el estado TIME_WAIT durante un breve período de tiempo. Si se usa con frecuencia una ruta de acceso al código que crea y elimina objetos HttpClient, la aplicación puede agotar los sockets disponibles. HttpClientFactory se introdujo en ASP.NET Core 2.1 como solución a este problema. Controla la agrupación de conexiones HTTP para optimizar el rendimiento y la confiabilidad. Para obtener más información, consulte el artículo sobre el Uso de HttpClientFactory para implementar solicitudes HTTP resistentes.

Recomendaciones:

Mantenimiento de rutas de acceso al código comunes rápidas

Quiere que todo el código sea rápido. Las rutas de acceso al código a las que se llama con frecuencia son las más críticas que se deben optimizar. Se incluyen los siguientes:

  • Los componentes de middleware de la canalización de procesamiento de solicitudes de la aplicación, especialmente el middleware, se ejecutan al principio de la canalización. Estos componentes tienen un gran impacto en el rendimiento.
  • Código que se ejecuta para cada solicitud o varias veces por solicitud. Por ejemplo, registro personalizado, controladores de autorización o inicialización de servicios transitorios.

Recomendaciones:

Completar tareas de larga duración fuera de las solicitudes HTTP

La mayoría de las solicitudes a una aplicación de ASP.NET Core se pueden controlar mediante un controlador o modelo de página que llama a los servicios necesarios y devuelve una respuesta HTTP. Para algunas solicitudes que implican tareas de larga duración, es mejor que todo el proceso de solicitud-respuesta sea asincrónico.

Recomendaciones:

  • No espere a que se completen las tareas de larga duración como parte del procesamiento de solicitudes HTTP normales.
  • Considere la posibilidad de controlar las solicitudes de larga duración con servicios en segundo plano o fuera de proceso con una función de Azure. Completar el trabajo fuera de proceso es especialmente beneficioso para las tareas que consumen mucha CPU.
  • Use opciones de comunicación en tiempo real, como SignalR, para comunicarse con los clientes de forma asincrónica.

Minificar los recursos del cliente

Las aplicaciones de ASP.NET Core con servidores front-end complejos a menudo proporcionan muchos archivos de JavaScript, CSS o de imagen. El rendimiento de las solicitudes de carga iniciales se puede mejorar mediante:

  • Agrupación, que combina varios archivos en uno.
  • Minificación, lo que reduce el tamaño de los archivos mediante la eliminación de espacios en blanco y comentarios.

Recomendaciones:

  • Use las instrucciones de agrupación y minificación, en las que se mencionan herramientas compatibles y se muestra cómo usar la etiqueta environment de ASP.NET Core para controlar los entornos de Development y Production.
  • Considere otras herramientas de terceros, como Webpack, para la administración compleja de recursos de cliente.

Compresión de respuestas

Reducir el tamaño de la respuesta suele aumentar la capacidad de respuesta de una aplicación, a menudo drásticamente. Una manera de reducir el tamaño de la carga útil es comprimir las respuestas de una aplicación. Para obtener más información, vea Compresión de respuesta.

Uso de la última versión de ASP.NET Core

Cada nueva versión de ASP.NET Core incluye mejoras de rendimiento. Las optimizaciones de .NET Core y ASP.NET Core significan que las versiones más recientes suelen superar el rendimiento de las versiones anteriores. Por ejemplo, .NET Core 2.1 agregó compatibilidad con expresiones regulares compiladas y se benefició de Span<T>. ASP.NET Core 2.2 agregó compatibilidad con HTTP/2. ASP.NET Core 3.0 agrega muchas mejoras que reducen el uso de memoria y aumentan el rendimiento. Si el rendimiento es una prioridad, considere la posibilidad de actualizar a la versión actual de ASP.NET Core.

Minimización de excepciones

Las excepciones deben ser poco frecuentes. La generación y detección de excepciones es lenta en relación con otros patrones de flujo de código. Por este motivo, las excepciones no se deben usar para controlar el flujo normal del programa.

Recomendaciones:

  • No use la generación ni la detección de excepciones como parte del flujo de programa normal, especialmente en las rutas de acceso al código activas.
  • Incluya lógica en la aplicación para detectar y controlar las condiciones que provocarían una excepción.
  • Genere o detecte excepciones para condiciones inusuales o inesperadas.

Las herramientas de diagnóstico de aplicaciones, como Application Insights, pueden ayudar a identificar excepciones comunes en una aplicación que pueden afectar al rendimiento.

Evitar la lectura o escritura sincrónicas en el cuerpo HttpRequest/HttpResponse

Todas las E/S de ASP.NET Core son asincrónicas. Los servidores implementan la interfaz Stream, que tiene sobrecargas sincrónicas y asincrónicas. Son más convenientes las asincrónicas para evitar bloquear los subprocesos del grupo de subprocesos. El bloqueo de subprocesos puede provocar un colapso del grupo de subprocesos.

No haga esto: en el ejemplo siguiente se usa ReadToEnd. Bloquea el subproceso actual para esperar el resultado. Este es un ejemplo de resultado sincrónico frente a asincrónico.

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

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

En el código anterior, Get lee sincrónicamente todo el cuerpo de la solicitud HTTP en la memoria. Si el cliente se carga lentamente, la aplicación está realizando una sincronización en lugar de una asincronización. La aplicación realiza la sincronización en lugar de la asincronización porque KestrelNO admite lecturas sincrónicas.

Haga esto: en el ejemplo siguiente se usa ReadToEndAsync y no se bloquea el subproceso mientras se lee.

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

}

El código anterior lee sincrónicamente todo el cuerpo de la solicitud HTTP en la memoria.

Advertencia

Si la solicitud es grande, la lectura del cuerpo completo de la solicitud HTTP en la memoria podría dar lugar a una condición de memoria insuficiente (OOM). OOM puede dar lugar a una denegación de servicio. Para obtener más información, vea Evitar la lectura de cuerpos de solicitud grandes o cuerpos de respuesta en memoria en este artículo.

Haga esto: el ejemplo siguiente es totalmente asincrónico mediante un cuerpo de solicitud no almacenado en búfer:

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

El código anterior deserializa de forma asincrónica el cuerpo de la solicitud en un objeto de C#.

Preferencia de ReadFormAsync over Request.Form

Use HttpContext.Request.ReadFormAsync en lugar de HttpContext.Request.Form. HttpContext.Request.Form solo se puede leer de forma segura con las condiciones siguientes:

  • El formulario se ha leído mediante una llamada a ReadFormAsync y
  • El valor del formulario almacenado en caché se está leyendo mediante HttpContext.Request.Form

No haga esto: en el ejemplo siguiente se usa HttpContext.Request.Form. HttpContext.Request.Form usa la sincronización en lugar de la asincronización y puede provocar el colapso del grupo de subprocesos.

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

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

        return Accepted();
    }

Haga esto: en el ejemplo siguiente se usa HttpContext.Request.ReadFormAsync para leer el cuerpo del formulario de forma asincrónica.

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

Evitar leer cuerpos de solicitud grandes o cuerpos de respuesta en la memoria

En .NET, cada asignación de objetos superior a 85 000 KB termina en el montón de objetos grandes (LOH). Los objetos grandes son costosos en dos aspectos:

En esta entrada de blog se describe el problema brevemente:

Cuando se asigna un objeto grande, se marca como objeto Gen 2. No Gen 0 como para objetos pequeños. Las consecuencias son que si se agota la memoria en LOH, GC limpia todo el montón administrado, no solo LOH. Por lo tanto, limpia Gen 0, Gen 1 y Gen 2, incluido LOH. Esto se denomina recolección completa de elementos no utilizados y es la recolección de elementos no utilizados que consume más tiempo. Para muchas aplicaciones, puede ser aceptable. Pero definitivamente no para los servidores web de alto rendimiento, donde se necesitan pocos búferes de memoria grande para controlar una solicitud web media (leer de un socket, descomprimir, descodificar JSON, etc.).

Almacenar un cuerpo de solicitud o respuesta grande en un único byte[] o string:

  • Puede dar lugar a que se agote rápidamente el espacio en la LOH.
  • Puede provocar problemas de rendimiento en la aplicación debido a la ejecución completa de los GC.

Trabajar con una API de procesamiento de datos sincrónica

Cuando se usa un serializador/deserializador que solo admite lecturas y escrituras sincrónicas (por ejemplo, Json.NET):

  • Almacene en búfer los datos en la memoria de forma asincrónica antes de pasarlos al serializador o deserializador.

Advertencia

Si la solicitud es grande, podría provocar una condición de memoria insuficiente (OOM). OOM puede dar lugar a una denegación de servicio. Para obtener más información, vea Evitar la lectura de cuerpos de solicitud grandes o cuerpos de respuesta en memoria en este artículo.

ASP.NET Core 3.0 usa System.Text.Json de forma predeterminada para la serialización de JSON. System.Text.Json:

  • Lee y escribe JSON de forma asincrónica.
  • Está optimizado para texto UTF-8.
  • Normalmente, el rendimiento es mayor que Newtonsoft.Json.

No almacenar IHttpContextAccessor.HttpContext en un campo

IHttpContextAccessor.HttpContext devuelve el valor HttpContext de la solicitud activa cuando se obtiene acceso desde el subproceso de solicitud. IHttpContextAccessor.HttpContextno se debe almacenar en un campo ni en una variable.

No haga esto: en el ejemplo siguiente se almacena HttpContext en un campo y, a continuación, se intenta usarlo más adelante.

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

El código anterior suele capturar un valor HttpContext null o incorrecto en el constructor.

Haga esto: en el ejemplo siguiente:

  • Almacena IHttpContextAccessor en un campo.
  • Usa el campo HttpContext en el momento correcto y comprueba 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");
        }
    }
}

Sin acceso a HttpContext desde varios subprocesos

HttpContextno es seguro con los subprocesos. El acceso a HttpContext desde varios subprocesos en paralelo puede dar lugar a un comportamiento inesperado, como no obtener respuestas, bloqueos y datos dañados.

No haga esto: en el ejemplo siguiente se realizan tres solicitudes paralelas y se registra la ruta de acceso de la solicitud entrante antes y después de la solicitud HTTP saliente. Se accede a la ruta de acceso de la solicitud desde varios subprocesos, posiblemente en 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;
    }

Haga esto: en el ejemplo siguiente se copian todos los datos de la solicitud entrante antes de realizar las tres solicitudes 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;
    }

No usar HttpContext una vez completada la solicitud

HttpContext solo es válido siempre que haya una solicitud HTTP activa en la canalización de ASP.NET Core. Toda la canalización de ASP.NET Core es una cadena asincrónica de delegados que ejecuta cada solicitud. Cuando se completa la devolución de Task desde esta cadena, HttpContext se recicla.

No haga esto: en el ejemplo siguiente se usa async void, que hace que la solicitud HTTP se complete cuando se alcanza el primer valor await:

  • El uso de async voidSIEMPRE es una práctica incorrecta en las aplicaciones de ASP.NET Core.
  • El código de ejemplo accede a HttpResponse una vez completada la solicitud HTTP.
  • El acceso retrasado bloquea el proceso.
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");
    }
}

Haga esto: en el ejemplo siguiente se devuelve un objeto Task al marco de trabajo, por lo que la solicitud HTTP no se completa hasta que se completa la acción.

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

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

No capturar HttpContext en subprocesos en segundo plano

No haga esto: en el ejemplo siguiente se muestra un cierre que captura el elemento HttpContext de la propiedad Controller. Se trata de una práctica incorrecta porque el elemento de trabajo podría:

  • Ejecutarse fuera del ámbito de la solicitud.
  • Intentar leer el elemento HttpContext incorrecto.
[HttpGet("/fire-and-forget-1")]
public IActionResult BadFireAndForget()
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

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

    return Accepted();
}

Haga esto: en el ejemplo siguiente:

  • Copia los datos necesarios en la tarea en segundo plano durante la solicitud.
  • No hace referencia a nada del 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();
}

Las tareas en segundo plano deben implementarse como servicios hospedados. Para más información, consulte Tareas en segundo plano con servicios hospedados.

No capturar los servicios insertados en los controladores en subprocesos en segundo plano

No haga esto: en el ejemplo siguiente se muestra un cierre que captura el elemento DbContext del parámetro de acción Controller. Esto es práctica no conveniente. El elemento de trabajo podría ejecutarse fuera del ámbito de la solicitud. ContosoDbContext está dentro del ámbito de la solicitud y da como resultado 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();
}

Haga esto: en el ejemplo siguiente:

  • Inserta IServiceScopeFactory para crear un ámbito en el elemento de trabajo en segundo plano. IServiceScopeFactory es un singleton.
  • Crea un ámbito de inserción de dependencias en el subproceso en segundo plano.
  • No hace referencia a nada del controlador.
  • No captura ContosoDbContext desde la solicitud entrante.
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

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

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

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

El código resaltado siguiente:

  • Crea un ámbito para la duración de la operación en segundo plano y resuelve los servicios pertinentes.
  • Usa ContosoDbContext desde el ámbito correcto.
[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();
}

No modificar el código de estado ni los encabezados después de que se haya iniciado el cuerpo de la respuesta

ASP.NET Core no almacena en búfer el cuerpo de la respuesta HTTP. La primera vez que se escribe la respuesta:

  • Los encabezados se envían junto con ese fragmento del cuerpo al cliente.
  • Ya no es posible cambiar los encabezados de respuesta.

No haga esto: el código siguiente intenta agregar encabezados de respuesta una vez iniciada la respuesta:

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

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

En el código anterior, context.Response.Headers["test"] = "test value"; producirá una excepción si next() se ha escrito en la respuesta.

Haga esto: en el ejemplo siguiente se comprueba si la respuesta HTTP se ha iniciado antes de modificar los encabezados.

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

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

Haga esto: en el ejemplo siguiente se usa HttpResponse.OnStarting para establecer los encabezados antes de que los encabezados de respuesta se vacíen en el cliente.

Comprobar si la respuesta no se ha iniciado permite registrar una devolución de llamada que se invocará justo antes de que se escriban los encabezados de respuesta. Comprobación de si la respuesta no se ha iniciado:

  • Proporciona la capacidad de anexar o invalidar encabezados cuando es necesario.
  • No requiere conocimiento del siguiente middleware en la canalización.
app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        context.Response.Headers["someheader"] = "somevalue";
        return Task.CompletedTask;
    });

    await next();
});

No llamar a next() si ya ha empezado a escribir en el cuerpo de la respuesta

Los componentes solo esperan llamarse si es posible que controlen y manipulen la respuesta.

Uso del hospedaje dentro del proceso con IIS

Con el hospedaje en proceso, una aplicación ASP.NET Core se ejecuta en el mismo proceso que su proceso de trabajo de IIS. El hospedaje dentro del proceso proporciona un rendimiento mejor que en el hospedaje fuera del proceso porque las solicitudes no se procesan mediante proxy a través del adaptador de bucle. El adaptador de bucle invertido es una interfaz de red que devuelve el tráfico de red saliente al mismo equipo. IIS controla la administración de procesos con el Servicio de activación de procesos de Windows (WAS).

Los proyectos tienen como valor predeterminado el modelo de hospedaje dentro del proceso en ASP.NET Core 3.0 y versiones posteriores.

Para más información, vea Hospedaje de ASP.NET Core en Windows con IIS.

No suponer que HttpRequest.ContentLength no es null

HttpRequest.ContentLength es null si el encabezado Content-Length no se recibe. Null en ese caso significa que no se conoce la longitud del cuerpo de la solicitud; no significa que la longitud sea cero. Dado que todas las comparaciones con null (excepto ==) devuelven false, la comparación Request.ContentLength > 1024, por ejemplo, podría devolver false cuando el tamaño del cuerpo de la solicitud es superior a 1024. No saber esto puede provocar vulnerabilidades de seguridad en las aplicaciones. Es posible que piense que se está protegiendo de solicitudes demasiado grandes cuando realmente no es así.

Para obtener más información, consulte la respuesta a StackOverflow.

Patrones de aplicación web confiable

Consulte El patrón de aplicación web confiable para .NETen los vídeos de YouTube y el artículo para obtener instrucciones sobre cómo crear una aplicación moderna, confiable, que se puede probar, rentable y escalable de ASP.NET Core, ya sea desde cero o refactorizando una aplicación existente.