Come leggere il corpo della richiesta in un controller webapi asp.net core?


105

Sto cercando di leggere il corpo della richiesta nel OnActionExecutingmetodo, ma ottengo sempre nullper il corpo.

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

Ho provato a impostare esplicitamente la posizione del flusso su 0 ma anche questo non ha funzionato. Poiché questo è ASP.NET CORE, le cose sono leggermente diverse, credo. Posso vedere tutti gli esempi qui riferiti alle vecchie versioni di webapi.
C'è un altro modo per farlo?


4
Fai attenzione, se il corpo della richiesta è stato letto già prima durante la pipeline della richiesta, allora è vuoto quando provi a leggerlo la seconda volta
Fabio

1

@Fabio Grazie per le informazioni, possiamo impostare la posizione e leggerlo di nuovo?
Kasun Koswattha

@KasunKoswattha - In base alla progettazione, il contenuto del corpo viene trattato come flusso solo in avanti che può essere letto una sola volta.
user270576

Immagino che la domanda si rivolga piuttosto ai filtri o al middleware che ai controller.
Jim Aho,

Risposte:


111

In ASP.Net Core sembra complicato leggere più volte la richiesta del corpo, tuttavia se il tuo primo tentativo lo fa nel modo giusto, dovresti andare bene per i tentativi successivi.

Ho letto diversi inversione di tendenza, ad esempio sostituendo il flusso corporeo, ma penso che il seguente sia il più pulito:

I punti più importanti sono

  1. per far sapere alla richiesta che ne leggerai il corpo due o più volte,
  2. per non chiudere il flusso del corpo e
  3. per riavvolgerlo nella posizione iniziale in modo che il processo interno non vada perso.

[MODIFICARE]

Come sottolineato da Murad, puoi anche sfruttare l'estensione .Net Core 2.1: EnableBufferingmemorizza le richieste di grandi dimensioni sul disco invece di tenerlo in memoria, evitando problemi di grandi flussi archiviati in memoria (file, immagini, ...) . È possibile modificare la cartella temporanea impostando ASPNETCORE_TEMPla variabile di ambiente ei file vengono eliminati al termine della richiesta.

In un AuthorizationFilter , puoi eseguire le seguenti operazioni:

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

Quindi puoi usare di nuovo il corpo nel gestore delle richieste.

Nel tuo caso, se ottieni un risultato nullo, probabilmente significa che il corpo è già stato letto in una fase precedente. In tal caso potrebbe essere necessario utilizzare un middleware (vedere di seguito).

Tuttavia fai attenzione se gestisci flussi di grandi dimensioni, quel comportamento implica che tutto viene caricato in memoria, questo non dovrebbe essere attivato in caso di caricamento di un file.

Puoi usarlo come middleware

Il mio assomiglia a questo (di nuovo, se scarichi / carichi file di grandi dimensioni, questo dovrebbe essere disabilitato per evitare problemi di 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>();
    }

}

lo stream è ancora vuoto anche se ritorno alla posizione 0.
Adrian Nasui

2
Hai usato req.EnableRewind();? Uso il codice sopra e funziona bene.
Jean

hanno usato req.EnableRewind (); non funziona. Ottengo Position = 0, body length = 26, ma leggendo il flusso "body" si ottiene una stringa vuota.
Adrian Nasui

Devi abilitare il riavvolgimento prima della prima lettura del corpo e non prima della seconda, se non lo fai penso che non funzionerà
Jean

3
È anche possibile utilizzare request.EnableBuffering()(wrapper over EnableRewind()) È disponibile in ASP.NET Core 2.1 docs.microsoft.com/en-us/dotnet/api/…
Murad

27

Una soluzione più chiara, funziona in ASP.Net Core 2.1 / 3.1

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

In un controller

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

2
Per netcore3.0, sarebbe .EnableBuffering () invece di.EnableRewind()
mr5

Grazie @ mr5 - Aggiornata la mia risposta
Andriod

L'ho trovato durante la correzione di alcuni aggiornamenti .net Core 2.2 -> Core 3.1 che hanno interrotto il modo EnableRewind (). Penso che questo abbia bisogno di un'altra riga di codice, senza la quale non potrei rileggere il corpo: HttpContext.Request.Body.Seek (0, SeekOrigin.Begin);
Larry Smith

2
Questo ha funzionato solo per me dopo essere passato AuthorizeAttributea Attribute(in ASP.Net Core 3.1).
Sigmund

Ragazzi, assicuratevi di aggiungere le librerie menzionate. Avevo già il codice ma EnableBuffering mostrava una linea ondulata rossa finché non mi sono reso conto che mancava il riferimento Microsoft.AspNetCore.Http. Grazie ad Android!
kaarthick raman

18

Per poter riavvolgere il corpo della richiesta, la risposta di @ Jean mi ha aiutato a trovare una soluzione che sembra funzionare bene. Attualmente lo uso per il middleware del gestore delle eccezioni globali, ma il principio è lo stesso.

Ho creato un middleware che sostanzialmente abilita il riavvolgimento sul corpo della richiesta (invece di un decoratore).

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

Questo può quindi essere usato nel tuo Startup.cssimile così:

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

Utilizzando questo approccio, sono stato in grado di riavvolgere correttamente il flusso del corpo della richiesta.


1
Questo ha funzionato benissimo per me @SaoBiz grazie! un errore di battitura, il cambiamento secondo questo a builder in UseEnableRequestRewind(this IApplicationBuilder builder).
Richard Logwood

@RichardLogwood Sono contento che abbia aiutato! Grazie per aver trovato l'errore di battitura! Fisso. :)
SaoBiz

6

Questo è un po 'un vecchio thread, ma da quando sono arrivato qui, ho pensato di pubblicare le mie scoperte in modo che potessero aiutare gli altri.

Innanzitutto, ho avuto lo stesso problema, in cui volevo ottenere la richiesta.Body e fare qualcosa con quello (registrazione / auditing). Ma per il resto volevo che l'endpoint avesse lo stesso aspetto.

Quindi, sembrava che la chiamata EnableBuffering () potesse fare il trucco. Quindi puoi fare una ricerca (0, xxx) sul corpo e rileggere i contenuti, ecc.

Tuttavia, questo ha portato al mio prossimo numero. Ricevo eccezioni "Le operazioni sincrone non sono consentite" quando si accede all'endpoint. Quindi, la soluzione alternativa è impostare la proprietà AllowSynchronousIO = true, nelle opzioni. Ci sono diversi modi per farlo (ma non è importante per i dettagli qui ..)

ALLORA, il prossimo numero è che quando vado a leggere la richiesta, il corpo è già stato eliminato. Uffa. Allora, cosa succede?

Sto usando Newtonsoft.JSON come parser [FromBody] nella chiamata endpiont. Questo è ciò che è responsabile delle letture sincrone e chiude anche il flusso quando ha finito. Soluzione? Leggere il flusso prima che arrivi all'analisi JSON? Certo, funziona e ho finito con questo:

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

Quindi ora posso accedere al corpo utilizzando HttpContext.Items ["request_body"] negli endpoint che hanno l'attributo [ReadRequestBodyIntoItems].

Ma amico, sembra che ci siano troppi cerchi da superare. Quindi ecco dove sono finito e ne sono davvero felice.

Il mio endpoint è iniziato come qualcosa di simile:

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

Ma è molto più semplice cambiare la firma, in questo modo:

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

Mi è piaciuto molto perché legge il flusso del corpo solo una volta e ho il controllo della deserializzazione. Certo, è bello se ASP.NET core fa questa magia per me, ma qui non perdo tempo a leggere lo stream due volte (forse buffering ogni volta), e il codice è abbastanza chiaro e pulito.

Se hai bisogno di questa funzionalità su molti endpoint, forse gli approcci middleware potrebbero essere più puliti, oppure puoi almeno incapsulare l'estrazione del corpo in una funzione di estensione per rendere il codice più conciso.

Ad ogni modo, non ho trovato alcuna fonte che abbia toccato tutti e 3 gli aspetti di questo problema, da qui questo post. Spero che questo aiuti qualcuno!

BTW: questo utilizzava ASP .NET Core 3.1.


Se il programma non è in grado di analizzare la stringa JSON in NyObjectType, non posso leggere il valore da "request_body"
Ericyu67

2

Ho riscontrato un problema simile durante l'utilizzo di ASP.NET Core 2.1:

  • Ho bisogno di un middleware personalizzato per leggere i dati POST ed eseguire alcuni controlli di sicurezza su di essi
  • l'utilizzo di un filtro di autorizzazione non è pratico, a causa del gran numero di azioni interessate
  • Devo consentire l'associazione di oggetti nelle azioni ([FromBody] someObject). Grazie a SaoBizper aver segnalato questa soluzione.

Quindi, la soluzione più ovvia è consentire il riavvolgimento della richiesta, ma assicurarsi che dopo aver letto il corpo, l'associazione funzioni ancora.

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

(posizionalo all'inizio del metodo Configure)

app.UseMiddleware<EnableRequestRewindMiddleware>();

Qualche altro middleware

Questo fa parte del middleware che richiede il disimballaggio delle informazioni inviate tramite POST per il controllo delle cose.

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

2

Recentemente mi sono imbattuto in una soluzione molto elegante che prende in modo casuale JSON di cui non hai idea la struttura:

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

Proprio così facile.


1

Il IHttpContextAccessormetodo funziona se desideri seguire questa strada.

TLDR;

  • Iniettare il IHttpContextAccessor

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

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

Altro - Un tentativo di un esempio conciso, non compilabile, degli elementi di cui devi assicurarti che siano a posto per ottenere un file IHttpContextAccessor. Le risposte hanno indicato correttamente che dovrai tornare all'inizio quando proverai a leggere il corpo della richiesta. Il CanSeek, Positionproprietà sul corpo della richiesta streaming utile per verificare questo.

Documenti di .NET Core DI

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

1

Sono stato in grado di leggere il corpo della richiesta in un'applicazione asp.net core 3.1 come questa (insieme a un semplice middleware che abilita il buffering -enable rewinding sembra funzionare per le versioni precedenti di .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;

0

per leggere Body, puoi leggere in modo asincrono.

usa il asyncmetodo come segue:

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

Prova con il postino:

inserisci qui la descrizione dell'immagine

Funziona bene e testato nella Asp.net coreversione 2.0 , 2.1 , 2.2, 3.0.

Spero sia utile


0

Volevo anche leggere Request.Body senza mapparlo automaticamente a un modello di parametri di azione. Ho provato molti modi diversi prima di risolverlo. E non ho trovato nessuna soluzione di lavoro descritta qui. Questa soluzione è attualmente basata sul framework .NET Core 3.0.

reader.readToEnd () sembrava un modo semplice, anche se compilato, ha generato un'eccezione di runtime che mi ha richiesto di usare una chiamata asincrona. Quindi invece ho usato ReadToEndAsync (), tuttavia a volte ha funzionato, a volte no. Dandomi errori come, impossibile leggere dopo la chiusura del flusso. Il problema è che non possiamo garantire che restituirà il risultato nello stesso thread (anche se usiamo l'attesa). Quindi abbiamo bisogno di una sorta di richiamata. Questa soluzione ha funzionato per me.

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

0

Il modo più semplice possibile per farlo è il seguente:

  1. Nel metodo Controller da cui estrarre il corpo, aggiungere questo parametro: [FromBody] SomeClass value

  2. Dichiara "SomeClass" come: class SomeClass {public string SomeParameter {get; impostato; }}

Quando il corpo grezzo viene inviato come json, .net core sa come leggerlo molto facilmente.


0

A coloro che vogliono semplicemente ottenere il contenuto (corpo della richiesta) dalla richiesta:

Utilizza l' [FromBody]attributo nel parametro del metodo del controller.

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

Come dice il documento: questo attributo specifica che un parametro o una proprietà deve essere associata utilizzando il corpo della richiesta.


0

Un modo rapido per aggiungere il buffer di risposta in .NET Core 3.1 è

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

in Startup.cs. Ho scoperto che questo garantisce anche che il buffering sarà abilitato prima che il flusso sia stato letto, il che era un problema per .Net Core 3.1 con alcune delle altre risposte di filtro middleware / autorizzazione che ho visto.

Quindi puoi leggere il corpo della tua richiesta tramite HttpContext.Request.Bodynel tuo gestore come molti altri hanno suggerito.

Vale anche la pena considerare che EnableBufferingha sovraccarichi che ti consentono di limitare la quantità di buffer in memoria prima di utilizzare un file temporaneo e anche un limite generale per il buffer. NB se una richiesta supera questo limite verrà lanciata un'eccezione e la richiesta non raggiungerà mai il tuo gestore.


Questo ha funzionato brillantemente per me (3.1). Ti ho citato su una domanda diversa: stackoverflow.com/a/63525694/6210068
Lance
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.