El cuerpo de la solicitud llega como un stream que solo se puede leer hacia adelante, y quien lo lee primero suele ser el único que lo obtiene. Ese único hecho lo decide todo al leer el cuerpo de la solicitud en una Web API de ASP.NET Core: dónde podemos leerlo, si el enlace de modelos (model binding) sigue funcionando después y por qué una segunda lectura sale vacía.

Para descargar el código fuente de este artículo, puedes visitar nuestro repositorio de GitHub.

Veremos cuatro lugares donde hacerlo: una acción de controlador, un middleware personalizado, un filtro de acción y un atributo personalizado. Request.EnableBuffering() es lo que permite que más de uno de ellos tenga éxito en la misma solicitud.

¿Cómo leer el cuerpo de la solicitud en una acción de controlador?

Request.Body, dentro de una acción de controlador, es el stream en bruto de la solicitud. Solo avanza hacia adelante y, en cuanto algo lo ha leído, queda situado al final, por lo que una segunda lectura sale vacía en lugar de lanzar una excepción.

Para leerlo una vez basta con un StreamReader. Para leerlo dos veces hace falta Request.EnableBuffering(), que sustituye el stream por uno que se puede rebobinar, y además un Request.Body.Position = 0 entre las dos lecturas.

La alternativa es dejar que el framework lo lea por nosotros. Un formateador de entrada deserializa en nuestro propio tipo el parámetro marcado con [FromBody] antes de que se ejecute la acción, así que trabajamos con un objeto y no con un stream.

Las dos opciones no se combinan por sí solas. Una vez que el enlace de modelos ha leído el cuerpo para un parámetro [FromBody], el stream dentro de esa misma acción está vacío, salvo que se haya almacenado en búfer antes y después se haya rebobinado.

Así que, dentro de una acción, la elección es sencilla: tomar el texto en bruto o tomar un modelo enlazado. Si queremos las dos cosas en una misma acción, para eso existe EnableBuffering().

Todo lo que sigue se ejecuta en un controlador normal de una Web API de ASP.NET Core, sin paquetes adicionales.

Método de extensión ReadAsStringAsync()

En lugar de convertir directamente los datos del stream en una cadena dentro de la acción del controlador, podemos implementar un método de extensión. Este enfoque nos permite usarlo en distintos casos. A medida que avancemos en el artículo, necesitaremos convertir una y otra vez los datos de un stream en una cadena. Los métodos de extensión ofrecen una solución eficiente: se implementan una vez y se usan muchas veces.

Vamos a implementar el método de extensión:

public static class RequestExtensions
{
    public static async Task<string> ReadAsStringAsync(this Stream requestBody, bool leaveOpen = false)
    {
        using StreamReader reader = new(requestBody, leaveOpen: leaveOpen);
        var bodyAsString = await reader.ReadToEndAsync();

        return bodyAsString;
    }
}

Creamos un método de extensión llamado ReadAsStringAsync(). Este método amplía las funcionalidades del tipo Stream y nos permite convertir los datos de un stream en una cadena. Para ello, creamos una instancia de la clase StreamReader. La clase StreamReader facilita la lectura de caracteres de un stream, ya sea un archivo o un stream de red.

Para entender mejor StreamReader y StreamWriter, recomendamos consultar nuestro artículo sobre cómo leer y escribir archivos con StreamWriter y StreamReader.

Aunque el constructor ofrece numerosas opciones, en nuestro caso le pasamos dos parámetros a StreamReader. El primero son los datos del stream que representan el cuerpo de la solicitud y el segundo, leaveOpen. Este parámetro garantiza que el stream siga abierto incluso después de que StreamReader termine sus operaciones y se libere.

En el paso siguiente, llamamos al método ReadToEndAsync() del objeto reader, que devuelve la representación en cadena de los datos del stream.

Leer como cadena

Podemos leer el cuerpo de la solicitud como cadena en una acción de controlador. Nuestras acciones van en un HomeController que añadimos a un proyecto creado con dotnet new webapi --use-controllers. Lleva los mismos atributos [ApiController] y [Route("[controller]")] que el controlador de la propia plantilla, así que la acción siguiente responde a las solicitudes POST en /Home/read-as-string. ASP.NET Core ofrece, dentro de las acciones de controlador, la propiedad Request, que da acceso a Request.Body. Sin embargo, este cuerpo es de tipo Stream y no podemos leerlo tal cual. Para manejar los datos de este stream como texto, tenemos que convertirlos en una cadena:

[HttpPost("read-as-string")]
public async Task<IActionResult> ReadAsString()
{
    var requestBody = await Request.Body.ReadAsStringAsync();

    return Ok($"Request Body As String: {requestBody}");
}

Aquí accedemos al cuerpo de la solicitud llamando al método de extensión ReadAsStringAsync() que hemos implementado antes. Cuando obtenemos la respuesta del método de extensión, la devolvemos directamente desde la acción con fines de prueba.

Usar EnableBuffering para varias lecturas

¿Qué ocurre si intentamos leer el cuerpo de la solicitud una vez más, o varias veces, en el caso anterior?

Vamos a comprobarlo:

[HttpPost("read-as-string-multiple")]
public async Task<IActionResult> ReadAsStringMultiple()
{
    var requestBody = await Request.Body.ReadAsStringAsync();
    var requestBodySecond = await Request.Body.ReadAsStringAsync();

    return Ok($"First: {requestBody}, Second:{requestBodySecond}");
}

Ahora, vamos a enviar la cadena “CodeMaze” a este método de acción como payload de la solicitud y a examinar la respuesta:

First: CodeMaze, Second:

El primer intento de lectura funciona, pero el segundo no da lo que esperamos.

Aquí no hay una página de Swagger UI en la que hacer clic, porque la plantilla Web API de .NET 10 incluye Microsoft.AspNetCore.OpenApi y no Swashbuckle, así que el proyecto genera un documento OpenAPI, pero no una página interactiva. Ese documento se genera a partir de todos los endpoints mapeados, y excluir un endpoint del documento OpenAPI generado es una tarea breve aparte.

Cuando hace falta leer varias veces el cuerpo de la solicitud, es imprescindible habilitar el almacenamiento en búfer. Para ello, podemos usar el método EnableBuffering() del objeto Request:

[HttpPost("read-multiple-enable-buffering")]
public async Task<IActionResult> ReadMultipleEnableBuffering()
{
    Request.EnableBuffering();
    var requestBody = await Request.Body.ReadAsStringAsync(true);

    Request.Body.Position = 0;
    var requestBodySecond = await Request.Body.ReadAsStringAsync();

    return Ok($"First: {requestBody}, Second:{requestBodySecond}");
}

Aquí llamamos al método Request.EnableBuffering(), que permite leer el cuerpo de la solicitud varias veces. A continuación, llamamos al método ReadAsStringAsync(). Para que el stream siga abierto en las lecturas siguientes, establecemos el parámetro leaveOpen en true. Justo antes del segundo intento, restablecemos a cero la posición del cuerpo de la solicitud.

Con estos últimos cambios aplicados, vamos a probar la API con el mismo payload:

First: CodeMaze, Second:CodeMaze

Después de llamar al método EnableBuffering(), obtenemos correctamente el cuerpo de la solicitud también en el segundo intento.

Enlace de modelos

ASP.NET Core permite deserializar automáticamente el cuerpo de la solicitud en una clase de modelo predefinida. Este enfoque simplifica el manejo de datos estructurados. Para aprovechar esta interesante característica, usamos el atributo [FromBody] en la acción, delante del parámetro del modelo:

[HttpPost("read-from-body")]
public IActionResult ReadFromBody([FromBody] PersonItemDto model)
{
    var message = $"Person Data => Name: {model.Name}, Age: {model.Age}";

    return Ok(message);
}

Aquí, ASP.NET Core mapea automáticamente el cuerpo de la solicitud entrante al tipo PersonItemDto. Después, dentro del cuerpo de la acción, podemos acceder a las propiedades del modelo y usarlas. Que el payload cumpla nuestras reglas es otra cuestión, y para comprobar si el modelo se ha enlazado de verdad está ModelState.

Podemos diseñar libremente el tipo PersonItemDto y sus propiedades para que se ajusten exactamente a las necesidades de la aplicación:

public record PersonItemDto(string Name, int Age);

Veamos un caso más antes de despedirnos de este tema. Supongamos que queremos enviar a nuestro método de acción un parámetro adicional con el salario y leerlo. La versión siguiente no funciona, y vale la pena ver por qué:

[HttpPost("read-from-body-multi-param")]
public IActionResult ReadFromBodyMultiParam([FromBody] PersonItemDto model, [FromBody] decimal salary)
{
    var message = $"Person Data => Name: {model.Name}, Age: {model.Age}, Salary: {salary}";

    return Ok(message);
}

Al compilar el código no aparece ningún error. Sin embargo, en tiempo de ejecución, cuando la aplicación intenta ejecutarse, se produce una excepción InvalidOperationException en la línea app.MapControllers() del archivo Program.cs:

Action 'ReadingRequestBody.Controllers.HomeController.ReadFromBodyMultiParam (ReadingRequestBody)'
has more than one parameter that was specified or inferred as bound from request body. Only one
parameter per action may be bound from body. Inspect the following parameters, and use
'FromQueryAttribute' to specify bound from query, 'FromRouteAttribute' to specify bound from route,
and 'FromBodyAttribute' for parameters to be bound from body:

Dicho de forma sencilla, esta excepción nos avisa de que el atributo FromBody solo se permite una vez entre los parámetros de un método de acción. Por eso, conviene reunir todos los parámetros en un único modelo de solicitud que se reciba como parámetro.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

¿Por qué solo se puede enlazar un parámetro desde el cuerpo de la solicitud?

Una acción puede tener como máximo un parámetro enlazado desde el cuerpo de la solicitud. Con [ApiController], un segundo parámetro hace que se lance InvalidOperationException mientras la aplicación arranca, en la línea app.MapControllers(), antes de que lleguen solicitudes.

El motivo es, de nuevo, el stream. La documentación de Microsoft sobre el enlace de modelos lo dice claramente: una vez que un formateador de entrada ha leído el stream de la solicitud, ya no está disponible para volver a leerlo y enlazar otros parámetros [FromBody].

El atributo [ApiController] hace más fácil toparse con esto sin querer, porque infiere el cuerpo como origen para los parámetros de tipo complejo sin que escribamos [FromBody] en absoluto. Basta con dos parámetros complejos en una acción para provocarlo.

La solución es reunir todo lo que lleva el payload en un único tipo y enlazar ese tipo. Los valores que en realidad no forman parte del payload van en otro sitio: [FromRoute] para los identificadores de la ruta, [FromQuery] para los filtros y [FromHeader] para los metadatos.

El mensaje de la excepción menciona FromQueryAttribute y FromRouteAttribute por su nombre precisamente por eso.

La documentación de Microsoft sobre el enlace de modelos es tajante al respecto: “Una vez que un formateador de entrada lee el stream de la solicitud, ya no está disponible para volver a leerse y enlazar otros parámetros [FromBody]”.

La inferencia es lo que convierte esta regla en una trampa, así que vale la pena saber exactamente cuándo el atributo ApiController elige por nosotros un origen de enlace.

¿Cómo leer el cuerpo de la solicitud en un middleware personalizado?

El middleware ve la solicitud antes del enlace de modelos y antes de cualquier filtro. Eso lo convierte en el primer punto en el que podemos leer el cuerpo y en el único que puede habilitar el almacenamiento en búfer para todo lo que viene después.

Es el lugar natural para el trabajo que se aplica a todas las solicitudes: registrar los payloads, auditar, calcular una firma o rechazar una solicitud directamente antes de que MVC llegue a verla.

Dos líneas hacen que sea seguro para el resto del pipeline. Request.EnableBuffering() sustituye el cuerpo por un stream que se puede rebobinar, y Request.Body.Position = 0 lo devuelve al principio cuando terminamos.

Si nos saltamos el rebobinado, todos los parámetros [FromBody] posteriores se enlazan a partir de un stream vacío, sin ninguna excepción que explique por qué.

El almacenamiento en búfer no es gratis. Si se llama sin argumentos, EnableBuffering() mantiene hasta 30 KB en memoria y escribe en un archivo temporal en disco todo lo que supere ese tamaño, así que un middleware que almacena en búfer todas las solicitudes también almacena en búfer todas las subidas.

Aquí no vamos a entrar en los detalles del middleware. Para repasarlo, consulta nuestro artículo sobre cómo crear flujos de aplicación flexibles con middleware de ASP.NET Core.

Veamos cómo crear un middleware personalizado para leer el cuerpo de la solicitud:

public class RequestBodyMiddleware(RequestDelegate next, ILogger<RequestBodyMiddleware> logger)
{
    private const int MaxContentLength = 1024;

    public async Task Invoke(HttpContext context)
    {
        await next(context);
    }
}

Aquí creamos un middleware personalizado llamado RequestBodyMiddleware. El constructor recibe ILogger<RequestBodyMiddleware> en lugar del ILogger no genérico, que el contenedor no puede resolver por sí solo: si pedimos ese, la aplicación lanza una excepción al arrancar.

Ahora vamos a implementar el método Invoke() para acceder al cuerpo de la solicitud y leerlo:

public async Task Invoke(HttpContext context)
{
    var requestPath = context.Request.Path.Value ?? string.Empty;

    if (requestPath.Contains("read-from-middleware", StringComparison.OrdinalIgnoreCase))
    {
        if (context.Request.ContentLength > MaxContentLength)
        {
            context.Response.StatusCode = StatusCodes.Status413PayloadTooLarge;
            await context.Response.WriteAsync("Request Body Too Large");

            return;
        }

        context.Request.EnableBuffering(bufferThreshold: MaxContentLength, bufferLimit: MaxContentLength);
        var requestBody = await context.Request.Body.ReadAsStringAsync(true);

        logger.LogInformation("Request Body:{@requestBody}", requestBody);
        context.Request.Headers.Append("RequestBodyMiddleware", requestBody);
        context.Items.Add("RequestBody", requestBody);
        context.Request.Body.Position = 0;
    }

    await next(context);
}

Primero examinamos la ruta de la solicitud para identificar rutas concretas. Después, convertimos el stream del cuerpo de la solicitud en una cadena en bruto llamando a nuestro método de extensión ReadAsStringAsync(). A partir de ahí, hay muchas opciones para aprovechar el cuerpo de la solicitud: registrar el resultado, añadirlo al encabezado de la solicitud o pasarlo a lo largo del pipeline en HttpContext.Items, entre otros usos posibles. Cuando terminamos de procesar el cuerpo, pasamos la solicitud al siguiente middleware invocando el delegado next().

Para usar este middleware, tenemos que añadirlo en el archivo Program.cs:

var app = builder.Build();
app.UseMiddleware<RequestBodyMiddleware>();

Para ver lo que ha leído el middleware, añadimos una acción más al controlador, en una ruta que contiene read-from-middleware:

[HttpPost("read-from-middleware")]
public IActionResult ReadFromMiddleware()
{
    var requestBody = Request.Headers["RequestBodyMiddleware"];
    var message = $"Request Body From Middleware : {requestBody}";

    return Ok(message);
}

Evitar leer cuerpos de solicitud grandes

Manejar cuerpos de solicitud grandes en una aplicación web exige cautela por los posibles problemas de memoria, la degradación del rendimiento y el agotamiento de recursos. El riesgo de ataques de denegación de servicio con cuerpos deliberadamente grandes es lo más grave, y la comprobación que escribimos decide si quedamos expuestos a él.

Comprobar requestBody.Length después de la lectura no es un límite de tamaño. Para entonces, todo el payload ya es una cadena en memoria, que es justo lo que busca un ataque con cuerpos grandes. El límite tiene que ir antes de la lectura, y por eso nuestro middleware consulta primero el encabezado de la solicitud:

if (context.Request.ContentLength > MaxContentLength)
{
    context.Response.StatusCode = StatusCodes.Status413PayloadTooLarge;
    await context.Response.WriteAsync("Request Body Too Large");

    return;
}

context.Request.EnableBuffering(bufferThreshold: MaxContentLength, bufferLimit: MaxContentLength);

ContentLength viene del encabezado de la solicitud, así que la decisión no cuesta nada. bufferLimit es la red de seguridad para una solicitud que no envía la longitud, porque leer más allá de ese límite lanza una excepción IOException en lugar de llenar la memoria o el disco. Con MaxContentLength en 1024, un payload de 2000 bytes devuelve Payload Too Large (413) y uno de 500 bytes pasa con un 200.

ASP.NET Core ya tiene sus propios límites. El valor predeterminado de MaxRequestBodySize en Kestrel es 30 000 000 bytes, unos 28.6 MB, y [RequestSizeLimit] en una acción o IHttpMaxRequestBodySizeFeature en un middleware lo reemplazan por endpoint o por solicitud.

¿Cómo leer el cuerpo de la solicitud en un filtro de acción?

Los filtros de acción se ejecutan después del enlace de modelos, y ese orden determina todo lo que pueden ver. La documentación de Microsoft sobre filtros sitúa los filtros de recursos antes del enlace de modelos y los filtros de acción después.

Así que un filtro de acción que lee Request.Body en una acción sin parámetros obtiene el payload completo. El mismo filtro, en una acción con un parámetro [FromBody], obtiene una cadena vacía, porque el formateador de entrada ya ha vaciado el stream.

No se lanza ninguna excepción. El filtro lee cero bytes y sigue adelante, así que esta es una de las formas más silenciosas de perder datos en una Web API.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

Dos cambios lo hacen fiable. Habilita el almacenamiento en búfer antes, en un middleware o en un filtro de recursos, y establece Request.Body.Position = 0 en el filtro antes de leer.

Si lo único que queremos es el cuerpo antes del enlace, un filtro de recursos es el mejor punto de enganche. Vive en el mismo pipeline de MVC, con acceso a los mismos metadatos del controlador y de la acción, pero se ejecuta antes del enlace de modelos.

Para saber más sobre los filtros de acción, consulta nuestro artículo sobre cómo escribir y registrar filtros de acción.

Vamos a crear un filtro de acción personalizado:

using Microsoft.AspNetCore.Mvc.Filters;

public class ReadRequestBodyActionFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var requestPath = context.HttpContext.Request.Path.Value ?? string.Empty;

        if (requestPath.Contains("read-from-action-filter", StringComparison.OrdinalIgnoreCase))
        {
            var requestBody = await context.HttpContext.Request.Body.ReadAsStringAsync();
            context.HttpContext.Request.Headers.Append("ReadRequestBodyActionFilter", requestBody);
        }

        await next();
    }
}

En este caso, creamos un filtro de acción personalizado llamado ReadRequestBodyActionFilter que implementa la interfaz IAsyncActionFilter. Dentro del filtro, definimos el método OnActionExecutionAsync() para implementar nuestra lógica concreta. Después, examinamos la ruta de la solicitud y extraemos el cuerpo de la solicitud con el método de extensión ReadAsStringAsync(). Por último, añadimos el cuerpo al encabezado de la solicitud con la clave ReadRequestBodyActionFilter.

Para usar este filtro de acción, tenemos que registrarlo en el archivo Program.cs:

builder.Services.AddControllers(options =>
{
    options.Filters.Add<ReadRequestBodyActionFilter>();
});

El filtro solo lee el cuerpo en una ruta que contiene read-from-action-filter, así que añadimos ahí una acción que devuelve el encabezado:

[HttpPost("read-from-action-filter")]
public IActionResult ReadFromActionFilter()
{
    var requestBody = Request.Headers["ReadRequestBodyActionFilter"];
    var message = $"Request Body From Action Filter : {requestBody}";

    return Ok(message);
}

¿Cómo leer el cuerpo de la solicitud con un atributo personalizado?

Para interceptar las solicitudes entrantes, se puede combinar un atributo personalizado con filtros de acción y así modificar el comportamiento del pipeline de procesamiento de solicitudes.

Vamos a crear un atributo personalizado para examinar y leer el cuerpo de la solicitud:

using Microsoft.AspNetCore.Mvc.Filters;

[AttributeUsage(AttributeTargets.Method)]
public class ReadRequestBodyAttribute : Attribute, IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var requestBody = await context.HttpContext.Request.Body.ReadAsStringAsync();
        context.HttpContext.Request.Headers.Append("ReadRequestBodyAttribute", requestBody);

        await next();
    }
}

Aquí creamos un atributo personalizado llamado ReadRequestBodyAttribute que implementa IAsyncActionFilter, y leemos el cuerpo en OnActionExecutionAsync() con el mismo método de extensión ReadAsStringAsync() de antes. Después lo añadimos a los encabezados de la solicitud con la clave ReadRequestBodyAttribute. Aparte de la interfaz de filtro, es un atributo personalizado como cualquier otro.

El momento de ejecución es idéntico al de la sección anterior, así que se aplica la misma precaución: esto se ejecuta después del enlace de modelos y necesita que el almacenamiento en búfer se haya habilitado antes si la acción también enlaza un parámetro desde el cuerpo. Lo que aporta el atributo es el alcance. Un filtro de acción registrado en AddControllers() se ejecuta para todas las acciones y tiene que comprobar la ruta, mientras que un atributo solo se ejecuta donde se aplica.

Ahora ya podemos usar nuestro atributo personalizado. Para ello, tenemos que aplicarlo a la acción del controlador:

[HttpPost("read-from-attribute")]
[ReadRequestBody]
public IActionResult ReadFromAttribute()
{
    var requestBody = Request.Headers["ReadRequestBodyAttribute"];
    var message = $"Request Body From Attribute : {requestBody}";

    return Ok(message);
}

Aquí aplicamos nuestro atributo personalizado ReadRequestBody a la acción de controlador ReadFromAttribute(). Dentro de la acción, examinamos el encabezado de solicitud ReadRequestBodyAttribute y asignamos su contenido a la respuesta de la acción.

¿Qué enfoque conviene usar para leer el cuerpo de la solicitud?

Elige según dónde siga existiendo el cuerpo y qué alcance tenga la tarea.

Un middleware, para todo lo que se aplica a todas las solicitudes: logging, auditoría, límites de tamaño. Se ejecuta primero, es el único lugar que puede habilitar el almacenamiento en búfer para todo el pipeline y puede terminar una solicitud antes de que empiece MVC.

Un filtro de recursos, para la misma tarea limitada a MVC, cuando queremos los metadatos del controlador y de la acción pero seguimos necesitando el cuerpo antes de que el enlace de modelos lo consuma.

Un filtro de acción o un atributo personalizado, cuando leer el cuerpo es una responsabilidad transversal que afecta a un puñado de acciones. Ambos se ejecutan después del enlace de modelos, así que los dos necesitan que el almacenamiento en búfer se habilite en algún punto anterior cuando la acción enlaza su cuerpo.

La propia acción del controlador, cuando solo le importa a esa acción, y [FromBody] siempre que el payload tenga una estructura que valga la pena deserializar.

El orden que hay detrás de todo esto es el propio pipeline de filtros, y vale la pena ver cómo se ordenan los filtros antes de elegir un punto de enganche.

Dónde lo leemosCuándo se ejecutaQué ve de forma predeterminadaÚsalo para
Middleware personalizadoAntes del enlace de modelos y de todos los filtrosEl cuerpo completo, antes que nadieLogging, auditoría, límites de tamaño, todo lo que se aplique a todas las solicitudes
Filtro de recursos (IAsyncResourceFilter)Después de la autorización, antes del enlace de modelosEl cuerpo completoHabilitar el almacenamiento en búfer dentro de MVC, donde están disponibles los metadatos del controlador y de la acción
Enlace de modelos ([FromBody])Entre los filtros de recursos y los filtros de acciónUn objeto deserializado, no un streamCualquier payload con estructura
Filtro de acción (IAsyncActionFilter)Después del enlace de modelosNada, si la acción tiene un parámetro [FromBody]Trabajo transversal sobre un grupo de acciones
Atributo personalizado que implementa IAsyncActionFilterDespués del enlace de modelos, igual que el anteriorIgual que el anteriorEl mismo trabajo, limitado a acciones concretas en lugar de registrado globalmente
La propia acción del controladorEn último lugarEl cuerpo, salvo que se haya enlazado un parámetro desde élUna acción que necesita el texto en bruto

Conclusión

En este artículo, hemos visto varias formas de leer el cuerpo de la solicitud en una aplicación Web API de ASP.NET Core. Obtener el cuerpo de la solicitud leyéndolo en las acciones del controlador ofrece sencillez y control en los casos básicos.

Podemos usar un middleware personalizado cuando queremos amplias capacidades de interceptación global. Esto puede permitirnos registrar el cuerpo de la solicitud en un solo punto para los endpoints de la Web API. Además, un middleware personalizado permite manipular fácilmente tanto las solicitudes como las respuestas en un solo lugar.

Los filtros de acción son un buen candidato para encapsular lógica, y así las acciones del controlador ganan claridad y foco. Con los filtros de acción, abstraemos los detalles complejos del manejo del cuerpo de la solicitud. Esto permite que la acción del controlador se mantenga más limpia y centrada en su propósito principal.

También aprovechamos los atributos personalizados para un manejo especializado y declarativo. Esto nos da un control preciso sobre el procesamiento del cuerpo de las solicitudes. La idoneidad de cada enfoque depende de los requisitos concretos de la aplicación, que van desde el control básico hasta la necesidad de encapsulación y especialización.

Elijamos el que elijamos, el factor decisivo es el mismo: hasta qué punto del pipeline sigue estando disponible el cuerpo para leerlo.

Probado con .NET 10.0.10.