¿Cómo leer el cuerpo de la solicitud en un controlador webapi principal de asp.net?

104

Estoy tratando de leer el cuerpo de la solicitud en el OnActionExecutingmétodo, pero siempre obtengo nullel cuerpo.

var request = context.HttpContext.Request;
var stream = new StreamReader(request.Body);
var body = stream.ReadToEnd();

Intenté establecer explícitamente la posición de la transmisión en 0, pero eso tampoco funcionó. Dado que esto es ASP.NET CORE, las cosas son un poco diferentes, creo. Puedo ver todas las muestras aquí que se refieren a versiones antiguas de webapi.
¿Hay otra manera de hacer esto?

Kasun Koswattha
fuente
4
Tenga cuidado, si el cuerpo de la solicitud ya se leyó antes durante la canalización de la solicitud, entonces estará vacío cuando intente leerlo por segunda vez
Fabio
1
Posible duplicado de Read HttpContent en el controlador WebApi
Fabio
@Fabio Gracias por la información, ¿podemos fijar la posición y volver a leerla?
Kasun Koswattha
@KasunKoswattha: por diseño, el contenido del cuerpo se trata como una transmisión de solo avance que solo se puede leer una vez.
user270576
Supongo que la pregunta apunta más bien a los filtros o middleware que a los controladores.
Jim Aho

Respuestas:

111

En ASP.Net Core parece complicado leer varias veces la solicitud del cuerpo, sin embargo, si su primer intento lo hace de la manera correcta, debería estar bien para los próximos intentos.

Leí varios cambios, por ejemplo, sustituyendo el flujo del cuerpo, pero creo que el siguiente es el más limpio:

Los puntos más importantes son

  1. para que la solicitud sepa que leerá su cuerpo dos o más veces,
  2. para no cerrar la corriente del cuerpo, y
  3. para rebobinarlo a su posición inicial para que el proceso interno no se pierda.

[EDITAR]

Como señaló Murad, también puede aprovechar la extensión .Net Core 2.1: EnableBufferingalmacena solicitudes grandes en el disco en lugar de mantenerlas en la memoria, evitando problemas de flujos grandes almacenados en la memoria (archivos, imágenes, ...) . Puede cambiar la carpeta temporal configurando ASPNETCORE_TEMPla variable de entorno, y los archivos se eliminan una vez que finaliza la solicitud.

En un AuthorizationFilter , puede hacer lo siguiente:

// Helper to enable request stream rewinds
using Microsoft.AspNetCore.Http.Internal;
[...]
public class EnableBodyRewind : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var bodyStr = "";
        var req = context.HttpContext.Request;

        // Allows using several time the stream in ASP.Net Core
        req.EnableRewind(); 

        // Arguments: Stream, Encoding, detect encoding, buffer size 
        // AND, the most important: keep stream opened
        using (StreamReader reader 
                  = new StreamReader(req.Body, Encoding.UTF8, true, 1024, true))
        {
            bodyStr = reader.ReadToEnd();
        }

        // Rewind, so the core is not lost when it looks the body for the request
        req.Body.Position = 0;

        // Do whatever work with bodyStr here

    }
}



public class SomeController : Controller
{
    [HttpPost("MyRoute")]
    [EnableBodyRewind]
    public IActionResult SomeAction([FromBody]MyPostModel model )
    {
        // play the body string again
    }
}

Luego, puede usar el cuerpo nuevamente en el controlador de solicitudes.

En su caso, si obtiene un resultado nulo, probablemente significa que el cuerpo ya se ha leído en una etapa anterior. En ese caso, es posible que deba utilizar un software intermedio (consulte a continuación).

Sin embargo, tenga cuidado si maneja flujos grandes, ese comportamiento implica que todo está cargado en la memoria, esto no debería activarse en caso de que se cargue un archivo.

Es posible que desee utilizar esto como Middleware

El mío se ve así (nuevamente, si descarga / carga archivos grandes, esto debería estar desactivado para evitar problemas de memoria):

public sealed class BodyRewindMiddleware
{
    private readonly RequestDelegate _next;

    public BodyRewindMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try { context.Request.EnableRewind(); } catch { }
        await _next(context);
        // context.Request.Body.Dipose() might be added to release memory, not tested
    }
}
public static class BodyRewindExtensions
{
    public static IApplicationBuilder EnableRequestBodyRewind(this IApplicationBuilder app)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }

        return app.UseMiddleware<BodyRewindMiddleware>();
    }

}
Vaquero
fuente
la transmisión sigue vacía incluso si retrocedo a la posición 0.
Adrian Nasui
2
¿Has usado req.EnableRewind();? Utilizo el código anterior y funciona bien.
Jean
han utilizado req.EnableRewind (); No funciona. Obtengo Posición = 0, longitud del cuerpo = 26, pero al leer la secuencia de 'cuerpo' aparece una cadena vacía.
Adrian Nasui
Tienes que habilitar el rebobinado antes de la primera lectura del cuerpo y no antes de la segunda, si no lo haces, creo que no funcionará
Jean
3
También es posible usar request.EnableBuffering()(envoltorio EnableRewind()) Está disponible en ASP.NET Core 2.1 docs.microsoft.com/en-us/dotnet/api/…
Murad
27

Una solución más clara, funciona en ASP.Net Core 2.1 / 3.1

Clase de filtro

using Microsoft.AspNetCore.Authorization;
// For ASP.NET 2.1
using Microsoft.AspNetCore.Http.Internal;
// For ASP.NET 3.1
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;

public class ReadableBodyStreamAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        // For ASP.NET 2.1
        // context.HttpContext.Request.EnableRewind();
        // For ASP.NET 3.1
        // context.HttpContext.Request.EnableBuffering();
    }
}

En un controlador

[HttpPost]
[ReadableBodyStream]
public string SomePostMethod()
{
    //Note: if you're late and body has already been read, you may need this next line
    //Note2: if "Note" is true and Body was read using StreamReader too, then it may be necessary to set "leaveOpen: true" for that stream.
    HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);

    using (StreamReader stream = new StreamReader(HttpContext.Request.Body))
    {
        string body = stream.ReadToEnd();
        // body = "param=somevalue&param2=someothervalue"
    }
}
Andriod
fuente
2
Para netcore3.0, sería .EnableBuffering () en lugar de.EnableRewind()
mr5
Gracias @ mr5 - Actualicé mi respuesta
Andriod
Encontré esto mientras arreglaba algunas actualizaciones de .net Core 2.2 -> Core 3.1 que rompieron la forma EnableRewind (). Creo que esto necesita una línea más de código, sin la cual no podría volver a leer el cuerpo: HttpContext.Request.Body.Seek (0, SeekOrigin.Begin);
Larry Smith
2
Esto solo funcionó para mí después de cambiar AuthorizeAttributea Attribute(en ASP.Net Core 3.1).
Sigmund
Chicos, asegúrese de agregar las bibliotecas mencionadas. Ya tenía el código, pero EnableBuffering mostraba una línea ondulada roja hasta que me di cuenta de que faltaba la referencia Microsoft.AspNetCore.Http. ¡Gracias a Android!
kaarthick raman
18

Para poder rebobinar el cuerpo de la solicitud, la respuesta de @ Jean me ayudó a encontrar una solución que parece funcionar bien. Actualmente lo uso para el Middleware del controlador de excepciones globales, pero el principio es el mismo.

Creé un middleware que básicamente permite el rebobinado en el cuerpo de la solicitud (en lugar de un decorador).

using Microsoft.AspNetCore.Http.Internal;
[...]
public class EnableRequestRewindMiddleware
{
    private readonly RequestDelegate _next;

    public EnableRequestRewindMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        context.Request.EnableRewind();
        await _next(context);
    }
}

public static class EnableRequestRewindExtension
{
    public static IApplicationBuilder UseEnableRequestRewind(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<EnableRequestRewindMiddleware>();
    }
}

Esto luego se puede usar a su Startup.csgusto, así:

[...]
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    [...]
    app.UseEnableRequestRewind();
    [...]
}

Usando este enfoque, he podido rebobinar el flujo del cuerpo de la solicitud con éxito.

SaoBiz
fuente
1
Esto funcionó muy bien para mí @SaoBiz ¡ Gracias! un error tipográfico, el cambio segundo este al Constructor en UseEnableRequestRewind(this IApplicationBuilder builder).
Richard Logwood
@RichardLogwood ¡Me alegro de haber ayudado! ¡Gracias por encontrar el error tipográfico! Fijo. :)
SaoBiz
6

Este es un hilo un poco antiguo, pero desde que llegué aquí, pensé que publicaría mis hallazgos para que puedan ayudar a otros.

Primero, tuve el mismo problema, donde quería obtener el Request.Body y hacer algo con eso (registro / auditoría). Pero, por lo demás, quería que el punto final tuviera el mismo aspecto.

Entonces, parecía que la llamada EnableBuffering () podría hacer el truco. Luego puede hacer una búsqueda (0, xxx) en el cuerpo y volver a leer el contenido, etc.

Sin embargo, esto me llevó a mi próximo número. Obtendría excepciones de "Las operaciones sincronizadas no están permitidas" al acceder al punto final. Entonces, la solución alternativa es establecer la propiedad AllowSynchronousIO = true, en las opciones. Hay varias formas de lograr esto (pero no es importante detallar aquí ...)

ENTONCES, el siguiente problema es que cuando voy a leer la Solicitud, el cuerpo ya se ha eliminado. Ugh. Entonces, ¿qué pasa?

Estoy usando Newtonsoft.JSON como mi analizador [FromBody] en la llamada endpiont. Eso es lo que es responsable de las lecturas sincrónicas y también cierra la secuencia cuando está lista. ¿Solución? ¿Leer la transmisión antes de que llegue al análisis JSON? Claro, eso funciona y terminé con esto:

 /// <summary>
/// quick and dirty middleware that enables buffering the request body
/// </summary>
/// <remarks>
/// this allows us to re-read the request body's inputstream so that we can capture the original request as is
/// </remarks>
public class ReadRequestBodyIntoItemsAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (context == null) return;

        // NEW! enable sync IO beacuse the JSON reader apparently doesn't use async and it throws an exception otherwise
        var syncIOFeature = context.HttpContext.Features.Get<IHttpBodyControlFeature>();
        if (syncIOFeature != null)
        {
            syncIOFeature.AllowSynchronousIO = true;

            var req = context.HttpContext.Request;

            req.EnableBuffering();

            // read the body here as a workarond for the JSON parser disposing the stream
            if (req.Body.CanSeek)
            {
                req.Body.Seek(0, SeekOrigin.Begin);

                // if body (stream) can seek, we can read the body to a string for logging purposes
                using (var reader = new StreamReader(
                     req.Body,
                     encoding: Encoding.UTF8,
                     detectEncodingFromByteOrderMarks: false,
                     bufferSize: 8192,
                     leaveOpen: true))
                {
                    var jsonString = reader.ReadToEnd();

                    // store into the HTTP context Items["request_body"]
                    context.HttpContext.Items.Add("request_body", jsonString);
                }

                // go back to beginning so json reader get's the whole thing
                req.Body.Seek(0, SeekOrigin.Begin);
            }
        }
    }
}

Así que ahora puedo acceder al cuerpo usando HttpContext.Items ["request_body"] en los puntos finales que tienen el atributo [ReadRequestBodyIntoItems].

Pero hombre, esto parece demasiados obstáculos para saltar. Así que aquí es donde terminé, y estoy realmente feliz con eso.

Mi punto final comenzó como algo como:

[HttpPost("")]
[ReadRequestBodyIntoItems]
[Consumes("application/json")]
public async Task<IActionResult> ReceiveSomeData([FromBody] MyJsonObjectType value)
{
    val bodyString = HttpContext.Items["request_body"];
    // use the body, process the stuff...
}

Pero es mucho más sencillo simplemente cambiar la firma, así:

[HttpPost("")]
[Consumes("application/json")]
public async Task<IActionResult> ReceiveSomeData()
{
    using (var reader = new StreamReader(
           Request.Body,
           encoding: Encoding.UTF8,
           detectEncodingFromByteOrderMarks: false
    ))
    {
        var bodyString = await reader.ReadToEndAsync();

        var value = JsonConvert.DeserializeObject<MyJsonObjectType>(bodyString);

        // use the body, process the stuff...
    }
}

Realmente me gustó esto porque solo lee el flujo del cuerpo una vez y yo tengo el control de la deserialización. Claro, es bueno si el núcleo de ASP.NET hace esta magia por mí, pero aquí no pierdo el tiempo leyendo la transmisión dos veces (quizás almacenando en búfer cada vez), y el código es bastante claro y limpio.

Si necesita esta funcionalidad en muchos puntos finales, quizás los enfoques de middleware podrían ser más limpios, o al menos puede encapsular la extracción del cuerpo en una función de extensión para hacer que el código sea más conciso.

De todos modos, no encontré ninguna fuente que tocara los 3 aspectos de este problema, de ahí esta publicación. ¡Ojalá esto ayude a alguien!

Por cierto: esto estaba usando ASP .NET Core 3.1.

Shane Anderson
fuente
Si el programa no puede analizar la cadena JSON a NyObjectType, entonces no puedo leer el valor de "request_body"
Ericyu67
2

Tuve un problema similar al usar ASP.NET Core 2.1:

  • Necesito un middleware personalizado para leer los datos publicados y realizar algunas comprobaciones de seguridad.
  • el uso de un filtro de autorización no es práctico, debido a la gran cantidad de acciones que se ven afectadas
  • Tengo que permitir que los objetos se unan a las acciones ([FromBody] someObject). Gracias a SaoBizpor señalar esta solución.

Entonces, la solución obvia es permitir que la solicitud sea rebobinada, pero asegúrese de que después de leer el cuerpo, el enlace aún funcione.

EnableRequestRewindMiddleware

public class EnableRequestRewindMiddleware
{
    private readonly RequestDelegate _next;

    ///<inheritdoc/>
    public EnableRequestRewindMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task Invoke(HttpContext context)
    {
        context.Request.EnableRewind();
        await _next(context);
    }
}

Startup.cs

(coloque esto al principio del método Configure)

app.UseMiddleware<EnableRequestRewindMiddleware>();

Algún otro middleware

Esto es parte del middleware que requiere desempaquetar la información POST para verificar cosas.

using (var stream = new MemoryStream())
{
    // make sure that body is read from the beginning
    context.Request.Body.Seek(0, SeekOrigin.Begin);
    context.Request.Body.CopyTo(stream);
    string requestBody = Encoding.UTF8.GetString(stream.ToArray());

    // this is required, otherwise model binding will return null
    context.Request.Body.Seek(0, SeekOrigin.Begin);
}
Alexei
fuente
2

Recientemente me encontré con una solución muy elegante que toma JSON aleatorio de la que no tienes idea de la estructura:

    [HttpPost]
    public JsonResult Test([FromBody] JsonElement json)
    {
        return Json(json);
    }

Así de fácil.

naspinski
fuente
1

El IHttpContextAccessormétodo funciona si desea seguir esta ruta.

TLDR;

  • Inyectar el IHttpContextAccessor

  • Rebobinar - HttpContextAccessor.HttpContext.Request.Body.Seek(0, System.IO.SeekOrigin.Begin);

  • Leer - System.IO.StreamReader sr = new System.IO.StreamReader(HttpContextAccessor.HttpContext.Request.Body); JObject asObj = JObject.Parse(sr.ReadToEnd());

Más : un intento de un ejemplo conciso y no compilable de los elementos que debe asegurarse de que estén en su lugar para obtener un archivo utilizable IHttpContextAccessor. Las respuestas han señalado correctamente que deberá volver al principio cuando intente leer el cuerpo de la solicitud. Las propiedades CanSeek, Positionen el flujo del cuerpo de la solicitud son útiles para verificar esto.

.NET Core DI Docs

// First -- Make the accessor DI available
//
// Add an IHttpContextAccessor to your ConfigureServices method, found by default
// in your Startup.cs file:
// Extraneous junk removed for some brevity:
public void ConfigureServices(IServiceCollection services)
{
    // Typical items found in ConfigureServices:
    services.AddMvc(config => { config.Filters.Add(typeof(ExceptionFilterAttribute)); });
    // ...

    // Add or ensure that an IHttpContextAccessor is available within your Dependency Injection container
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

// Second -- Inject the accessor
//
// Elsewhere in the constructor of a class in which you want
// to access the incoming Http request, typically 
// in a controller class of yours:
public class MyResourceController : Controller
{
    public ILogger<PricesController> Logger { get; }
    public IHttpContextAccessor HttpContextAccessor { get; }

    public CommandController(
        ILogger<CommandController> logger,
        IHttpContextAccessor httpContextAccessor)
    {
        Logger = logger;
        HttpContextAccessor = httpContextAccessor;
    }

    // ...

    // Lastly -- a typical use 
    [Route("command/resource-a/{id}")]
    [HttpPut]
    public ObjectResult PutUpdate([FromRoute] string id, [FromBody] ModelObject requestModel)
    {
        if (HttpContextAccessor.HttpContext.Request.Body.CanSeek)
        {
            HttpContextAccessor.HttpContext.Request.Body.Seek(0, System.IO.SeekOrigin.Begin);
            System.IO.StreamReader sr = new System.IO.StreamReader(HttpContextAccessor.HttpContext.Request.Body);
            JObject asObj = JObject.Parse(sr.ReadToEnd());

            var keyVal = asObj.ContainsKey("key-a");
        }
    }
}    
Randy Larson
fuente
1

Pude leer el cuerpo de la solicitud en una aplicación asp.net core 3.1 como esta (junto con un middleware simple que permite el almacenamiento en búfer -el rebobinado parece funcionar para versiones anteriores de .Net Core-):

var reader = await Request.BodyReader.ReadAsync();
Request.Body.Position = 0;
var buffer = reader.Buffer;
var body = Encoding.UTF8.GetString(buffer.FirstSpan);
Request.Body.Position = 0;
ruzgarustu
fuente
0

para leer Body, puede leer de forma asincrónica.

use el asyncmétodo como sigue:

public async Task<IActionResult> GetBody()
{
      string body="";
      using (StreamReader stream = new StreamReader(Request.Body))
      {
           body = await stream.ReadToEndAsync();
      }
    return Json(body);
}

Prueba con cartero:

ingrese la descripción de la imagen aquí

Está funcionando bien y probado en Asp.net coreversión 2.0 , 2.1 , 2.2, 3.0.

Espero sea de utilidad.

AminRostami
fuente
0

También quería leer Request.Body sin asignarlo automáticamente a algún modelo de parámetro de acción. Probado de muchas formas diferentes antes de resolver esto. Y no encontré ninguna solución de trabajo descrita aquí. Esta solución se basa actualmente en el marco .NET Core 3.0.

reader.readToEnd () se asentó como una forma simple, a pesar de que se compiló, arrojó una excepción de tiempo de ejecución que requería que usara una llamada asíncrona. Entonces, en su lugar, usé ReadToEndAsync (), sin embargo, a veces funcionó y otras no. Dándome errores como, no se puede leer después de cerrar la transmisión. El problema es que no podemos garantizar que devolverá el resultado en el mismo hilo (incluso si usamos el await). Entonces necesitamos algún tipo de devolución de llamada. Esta solución funcionó para mí.

[Route("[controller]/[action]")]
public class MyController : ControllerBase
{

    // ...

    [HttpPost]
    public async void TheAction()
    {
        try
        {
            HttpContext.Request.EnableBuffering();
            Request.Body.Position = 0;
            using (StreamReader stream = new StreamReader(HttpContext.Request.Body))
            {
                var task = stream
                    .ReadToEndAsync()
                    .ContinueWith(t => {
                        var res = t.Result;
                        // TODO: Handle the post result!
                    });

                // await processing of the result
                task.Wait();
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to handle post!");
        }
    }
krazor
fuente
0

La forma más sencilla posible de hacerlo es la siguiente:

  1. En el método Controller del que necesita extraer el cuerpo, agregue este parámetro: [FromBody] Valor de SomeClass

  2. Declare "SomeClass" como: class SomeClass {cadena pública SomeParameter {get; conjunto; }}

Cuando el cuerpo en bruto se envía como json, .net core sabe cómo leerlo muy fácilmente.

abelabbesnabi
fuente
0

Para aquellos que simplemente quieren obtener el contenido (cuerpo de la solicitud) de la solicitud:

Utilice el [FromBody]atributo en el parámetro de método de su controlador.

[Route("api/mytest")]
[ApiController]
public class MyTestController : Controller
{
    [HttpPost]
    [Route("content")]
    public async Task<string> ReceiveContent([FromBody] string content)
    {
        // Do work with content
    }
}

Como dice el documento: este atributo especifica que un parámetro o propiedad debe vincularse utilizando el cuerpo de la solicitud.

sirazal
fuente
0

Una forma rápida de agregar búfer de respuesta en .NET Core 3.1 es

    app.Use((context, next) =>
    {
        context.Request.EnableBuffering();
        return next();
    });

en Startup.cs. Descubrí que esto también garantiza que el almacenamiento en búfer se habilitará antes de que se lea la transmisión, lo que fue un problema para .Net Core 3.1 con algunas de las otras respuestas de filtro de autorización / middleware que he visto.

Luego, puede leer el cuerpo de su solicitud a través HttpContext.Request.Bodyde su controlador, como varios otros han sugerido.

También vale la pena considerar que EnableBufferingtiene sobrecargas que le permiten limitar la cantidad de búfer en la memoria antes de que use un archivo temporal, y también un límite general para su búfer. Nota: si una solicitud excede este límite, se lanzará una excepción y la solicitud nunca llegará a su administrador.

Stephen Wilkinson
fuente
Esto funcionó de manera brillante para mí (3.1). Lo citó en una pregunta diferente: stackoverflow.com/a/63525694/6210068
Lance