Como ler o corpo da solicitação em um controlador webapi de núcleo asp.net?

105

Estou tentando ler o corpo da solicitação no OnActionExecutingmétodo, mas sempre consigo nullpelo corpo.

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

Tentei definir explicitamente a posição do fluxo para 0, mas também não funcionou. Como este é o ASP.NET CORE, as coisas são um pouco diferentes, eu acho. Posso ver todos os exemplos aqui referentes a versões antigas do webapi.
Existe alguma outra maneira de fazer isso?

Kasun Koswattha
fonte
4
Tenha cuidado, se o corpo da solicitação já foi lido antes durante o pipeline de solicitação, então ele está vazio quando você tenta lê-lo pela segunda vez
Fabio
1
Possível duplicata de Read HttpContent no controlador WebApi
Fabio
@Fabio Obrigado pela informação, podemos definir a posição e lê-la novamente?
Kasun Koswattha
@KasunKoswattha - Por design, o conteúdo do corpo é tratado como fluxo somente de encaminhamento que pode ser lido apenas uma vez.
user270576
Eu acho que a questão é mais direcionada a filtros ou middleware do que controladores.
Jim Aho

Respostas:

111

No ASP.Net Core parece complicado ler várias vezes a solicitação do corpo, porém se sua primeira tentativa for da maneira certa, você deve estar bem para as próximas tentativas.

Eu li várias reviravoltas, por exemplo, substituindo o fluxo do corpo, mas acho que o seguinte é o mais limpo:

Os pontos mais importantes são

  1. para deixar a solicitação saber que você lerá seu corpo duas ou mais vezes,
  2. para não fechar o fluxo do corpo, e
  3. para retroceder até sua posição inicial para que o processo interno não se perca.

[EDITAR]

Conforme apontado por Murad, você também pode aproveitar a extensão .Net Core 2.1: EnableBufferingela armazena grandes solicitações no disco em vez de mantê-lo na memória, evitando problemas de streams grandes armazenados na memória (arquivos, imagens, ...) . Você pode alterar a pasta temporária definindo ASPNETCORE_TEMPa variável de ambiente e os arquivos são excluídos assim que a solicitação terminar.

Em um AuthorizationFilter , você pode fazer o seguinte:

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

Em seguida, você pode usar o corpo novamente no manipulador de solicitação.

No seu caso, se você obtiver um resultado nulo, provavelmente significa que o corpo já foi lido em um estágio anterior. Nesse caso, você pode precisar usar um middleware (veja abaixo).

No entanto, tenha cuidado ao lidar com grandes fluxos, esse comportamento implica que tudo é carregado na memória, isso não deve ser acionado no caso de um upload de arquivo.

Você pode querer usar isto como um Middleware

O meu se parece com isto (novamente, se você fizer download / upload de arquivos grandes, isso deve ser desativado para evitar problemas de memória):

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

}
Jean
fonte
o stream ainda está vazio mesmo se eu voltar para a posição 0.
Adrian Nasui
2
Você já usou req.EnableRewind();? Eu uso o código acima e funciona bem.
Jean
usaram req.EnableRewind (); não funciona. Eu obtenho Posição = 0, comprimento do corpo = 26, mas lendo o fluxo de 'corpo' surge uma string vazia.
Adrian Nasui
Você tem que ativar o retrocesso antes da primeira leitura do corpo e não antes da segunda, se não, acho que não vai funcionar
Jean
3
Também é possível usar request.EnableBuffering()(wrapper over EnableRewind()) Ele está disponível no ASP.NET Core 2.1 docs.microsoft.com/en-us/dotnet/api/…
Murad
27

Uma solução mais clara, funciona em ASP.Net Core 2.1 / 3.1

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

Em um 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
fonte
2
Para netcore3.0, seria .EnableBuffering () em vez de.EnableRewind()
mr5
Obrigado @ mr5 - Atualizado minha resposta
Andriod
Eu descobri isso ao consertar algumas atualizações .net Core 2.2 -> Core 3.1 que quebraram o método EnableRewind (). Acho que isso precisa de mais uma linha de código, sem a qual não poderia reler o Corpo: HttpContext.Request.Body.Seek (0, SeekOrigin.Begin);
Larry Smith
2
Isso só funcionou para mim depois de mudar AuthorizeAttributepara Attribute(no ASP.Net Core 3.1).
Sigmund
Pessoal, por favor, certifique-se de adicionar as bibliotecas mencionadas. Eu já tinha o código, mas EnableBuffering estava mostrando uma linha vermelha irregular até que percebi que a referência Microsoft.AspNetCore.Http estava faltando. Graças ao Android!
Kaarthick Raman
18

Para poder retroceder o corpo do pedido, a resposta de @Jean me ajudou a encontrar uma solução que parece funcionar bem. Atualmente, eu uso isso para Global Exception Handler Middleware, mas o princípio é o mesmo.

Criei um middleware que basicamente permite o retrocesso no corpo da solicitação (em vez de um 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>();
    }
}

Isso pode ser usado da seguinte Startup.csmaneira:

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

Usando essa abordagem, consegui retroceder o fluxo do corpo da solicitação com êxito.

SaoBiz
fonte
1
Funcionou muito bem para mim @SaoBiz, obrigado! um erro de digitação, mudança 2º deste para construtor no UseEnableRequestRewind(this IApplicationBuilder builder).
Richard Logwood
@RichardLogwood Que bom que ajudou! Obrigado por encontrar o erro de digitação! Fixo. :)
SaoBiz
6

Este é um tópico um pouco antigo, mas desde que cheguei aqui, decidi postar minhas descobertas para que possam ajudar outras pessoas.

Primeiro, eu tive o mesmo problema, onde eu queria pegar o Request.Body e fazer algo com ele (registro / auditoria). Mas, caso contrário, eu queria que o ponto final fosse o mesmo.

Portanto, parecia que a chamada EnableBuffering () poderia resolver o problema. Então você pode fazer uma busca (0, xxx) no corpo e reler o conteúdo, etc.

No entanto, isso levou ao meu próximo problema. Eu obteria exceções "Operações síncronas não são permitidas" ao acessar o ponto de extremidade. Portanto, a solução alternativa é definir a propriedade AllowSynchronousIO = true, nas opções. Existem várias maneiras de fazer isso (mas não é importante detalhar aqui ..)

ENTÃO, o próximo problema é que quando vou ler o Request.Body ele já foi descartado. Ugh. Então, o que aconteceu?

Estou usando o Newtonsoft.JSON como meu analisador [FromBody] na chamada do endpiont. Isso é o que é responsável pelas leituras síncronas e também fecha o fluxo quando é concluído. Solução? Leia o fluxo antes de chegar à análise JSON? Claro, isso funciona e acabei com o seguinte:

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

Agora, posso acessar o corpo usando HttpContext.Items ["request_body"] nos pontos de extremidade que têm o atributo [ReadRequestBodyIntoItems].

Mas cara, isso parece muitos obstáculos para pular. Então foi aqui que eu terminei e estou muito feliz com isso.

Meu endpoint começou 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...
}

Mas é muito mais simples apenas alterar a assinatura, assim:

[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...
    }
}

Eu realmente gostei disso porque ele lê o fluxo do corpo apenas uma vez e eu tenho o controle da desserialização. Claro, é bom se o ASP.NET core fizer essa mágica para mim, mas aqui não perco tempo lendo o fluxo duas vezes (talvez armazenando em buffer a cada vez) e o código é bastante claro e limpo.

Se você precisar dessa funcionalidade em muitos terminais, talvez as abordagens de middleware possam ser mais limpas ou você pode pelo menos encapsular a extração do corpo em uma função de extensão para tornar o código mais conciso.

De qualquer forma, não encontrei nenhuma fonte que abordasse todos os 3 aspectos desse assunto, daí este post. Espero que isso ajude alguém!

BTW: isso estava usando ASP .NET Core 3.1.

Shane Anderson
fonte
Se o programa não consegue analisar a string JSON para NyObjectType, então não consigo ler o valor de "request_body"
Ericyu67
2

Tive um problema semelhante ao usar o ASP.NET Core 2.1:

  • Preciso de um middleware personalizado para ler os dados POSTados e realizar algumas verificações de segurança
  • usar um filtro de autorização não é prático, devido ao grande número de ações que são afetadas
  • Tenho que permitir a vinculação de objetos nas ações ([FromBody] someObject). Obrigado SaoBizpor apontar esta solução.

Portanto, a solução óbvia é permitir que a solicitação seja rebobinada, mas certifique-se de que, após a leitura do corpo, a vinculação ainda 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 isso no início do método de configuração)

app.UseMiddleware<EnableRequestRewindMiddleware>();

Algum outro middleware

Isso é parte do middleware que requer a descompactação das informações do POST para verificação.

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
fonte
2

Recentemente me deparei com uma solução muito elegante que pega em JSON aleatório da qual você não tem ideia da estrutura:

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

Simples assim.

Naspinski
fonte
1

O IHttpContextAccessormétodo funciona se você quiser seguir esse caminho.

TLDR;

  • Injetar o IHttpContextAccessor

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

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

Mais - uma tentativa de um exemplo conciso e não compilador dos itens que você precisa garantir que estejam no lugar para chegar a um utilizável IHttpContextAccessor. As respostas indicaram corretamente que você precisará voltar ao início ao tentar ler o corpo da solicitação. A CanSeek, Positionpropriedades no corpo da solicitação transmitir útil para verificar isso.

Documentos DI do .NET Core

// 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
fonte
1

Consegui ler o corpo da solicitação em um aplicativo asp.net core 3.1 como este (junto com um middleware simples que permite o armazenamento em buffer - o retrocesso habilitado parece estar funcionando para versões anteriores do .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
fonte
0

para leitura Body, você pode ler de forma assíncrona.

use o asyncmétodo a seguir:

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

Teste com carteiro:

insira a descrição da imagem aqui

Está funcionando bem e testado na Asp.net coreversão 2.0 , 2.1 , 2.2, 3.0.

Espero que seja útil.

AminRostami
fonte
0

Eu também queria ler o Request.Body sem mapeá-lo automaticamente para algum modelo de parâmetro de ação. Testei de várias maneiras diferentes antes de resolver isso. E não encontrei nenhuma solução funcional aqui descrita. Esta solução é atualmente baseada na estrutura do .NET Core 3.0.

reader.readToEnd () parece uma maneira simples, embora tenha compilado, ele lançou uma exceção de tempo de execução que exigia que eu usasse uma chamada assíncrona. Então, em vez disso, usei ReadToEndAsync (), mas funcionou às vezes e às vezes não. Apresentando erros como, não consigo ler depois que o fluxo é fechado. O problema é que não podemos garantir que ele retornará o resultado no mesmo thread (mesmo se usarmos o await). Portanto, precisamos de algum tipo de retorno de chamada. Essa solução funcionou para mim.

[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
fonte
0

A maneira mais simples possível de fazer isso é a seguinte:

  1. No método Controller de onde você precisa extrair o corpo, adicione este parâmetro: [FromBody] SomeClass value

  2. Declare o "SomeClass" como: class SomeClass {public string SomeParameter {get; conjunto; }}

Quando o corpo bruto é enviado como json, o .net core sabe como lê-lo com muita facilidade.

abelabbesnabi
fonte
0

Para aqueles que simplesmente desejam obter o conteúdo (corpo da solicitação) da solicitação:

Use o [FromBody]atributo em seu parâmetro de método do 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 diz o doc: este atributo especifica que um parâmetro ou propriedade deve ser vinculado usando o corpo da solicitação.

sirazal
fonte
0

Uma maneira rápida de adicionar buffer de resposta no .NET Core 3.1 é

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

em Startup.cs. Descobri que isso também garante que o buffer será habilitado antes que o stream seja lido, o que era um problema para o .Net Core 3.1 com algumas das outras respostas de filtro de middleware / autorização que vi.

Em seguida, você pode ler o corpo da solicitação via HttpContext.Request.Bodyem seu manipulador, como vários outros sugeriram.

Também vale a pena considerar que EnableBufferingtem sobrecargas que permitem limitar o quanto ele armazenará em buffer na memória antes de usar um arquivo temporário, e também um limite geral para o buffer. NB, se uma solicitação exceder esse limite, uma exceção será lançada e a solicitação nunca chegará ao seu manipulador.

Stephen Wilkinson
fonte
Isso funcionou brilhantemente para mim (3.1). Citou você sobre uma pergunta diferente: stackoverflow.com/a/63525694/6210068
Lance