Jak odczytać treść żądania w kontrolerze webapi asp.net core?

113

Próbuję odczytać treść żądania w OnActionExecutingmetodzie, ale zawsze otrzymuję treść żądania null.

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

Próbowałem jawnie ustawić pozycję strumienia na 0, ale to też nie zadziałało. Ponieważ jest to ASP.NET Core, myślę, że sytuacja wygląda trochę inaczej. Widzę tutaj wszystkie przykłady odnoszące się do starych wersji internetowego interfejsu API.

Czy można to zrobić w inny sposób?

Kasun Koswattha
źródło
5
Uważaj, jeśli treść żądania została odczytana już wcześniej podczas potoku żądań, to jest pusta przy próbie odczytania go po raz drugi
Fabio
1
Możliwy duplikat odczytu HttpContent w kontrolerze WebApi
Fabio
@Fabio Dzięki za informację, czy możemy ustawić pozycję i przeczytać jeszcze raz?
Kasun Koswattha
@KasunKoswattha - zgodnie z projektem treść jest traktowana jako strumień tylko do przodu, który można odczytać tylko raz.
user270576
Wydaje mi się, że pytanie dotyczy raczej filtrów lub oprogramowania pośredniczącego niż kontrolerów.
Jim Aho,

Odpowiedzi:

115

W ASP.Net Core kilkakrotne odczytanie żądania treści wydaje się skomplikowane, jednak jeśli pierwsza próba zrobi to we właściwy sposób, kolejne próby powinny być w porządku.

Czytałem kilka zwrotów, na przykład zastępując strumień ciała, ale myślę, że najczystszy jest następujący:

Najważniejsze punkty

  1. aby powiadomić żądanie, że dwukrotnie lub więcej razy przeczytasz jego treść,
  2. nie zamykać strumienia ciała, i
  3. przewinąć go do pozycji początkowej, aby nie zgubić wewnętrznego procesu.

[EDYTOWAĆ]

Jak zauważył Murad, możesz również skorzystać z rozszerzenia .Net Core 2.1: EnableBufferingprzechowuje duże żądania na dysku zamiast przechowywać je w pamięci, unikając problemów z dużymi strumieniami przechowywanymi w pamięci (pliki, obrazy, ...) . Możesz zmienić folder tymczasowy, ustawiając ASPNETCORE_TEMPzmienną środowiskową, a pliki są usuwane po zakończeniu żądania.

W AuthorizationFilter możesz wykonać następujące czynności:

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

Następnie możesz ponownie użyć treści w module obsługi żądań.

W twoim przypadku, jeśli otrzymasz wynik zerowy, prawdopodobnie oznacza to, że treść została już odczytana na wcześniejszym etapie. W takim przypadku może być konieczne użycie oprogramowania pośredniego (patrz poniżej).

Jednak zachowaj ostrożność, jeśli obsługujesz duże strumienie, to zachowanie oznacza, że ​​wszystko jest ładowane do pamięci, nie powinno to być wyzwalane w przypadku załadowania pliku.

Możesz użyć tego jako oprogramowania pośredniego

Mój wygląda tak (ponownie, jeśli pobierasz / przesyłasz duże pliki, powinno to być wyłączone, aby uniknąć problemów z pamięcią):

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

}
Drelich
źródło
strumień jest nadal pusty, nawet jeśli
przewinę
2
Czy używałeś req.EnableRewind();? Używam powyższego kodu i działa dobrze.
Jean,
1
użyły req.EnableRewind (); nie działa. Otrzymuję Position = 0, body length = 26, ale odczyt strumienia „body” daje pusty ciąg.
Adrian Nasui,
To doskonała odpowiedź
Gayan
3
Możliwe jest również użycie request.EnableBuffering()(wrapper over EnableRewind()) Jest dostępne w ASP.NET Core 2.1 docs.microsoft.com/en-us/dotnet/api/ ...
Murad
29

Bardziej przejrzyste rozwiązanie, działa w ASP.Net Core 2.1 / 3.1

Klasa filtra

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

W kontrolerze

[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
źródło
2
W przypadku netcore3.0 byłoby to .EnableBuffering () zamiast.EnableRewind()
mr5
Dzięki @ mr5 - Zaktualizowałem moją odpowiedź
Andriod
Znalazłem to podczas naprawiania niektórych aktualizacji .net Core 2.2 -> Core 3.1, które zepsuły sposób EnableRewind (). Myślę, że to wymaga jeszcze jednego wiersza kodu, bez którego nie mógłbym ponownie odczytać Body: HttpContext.Request.Body.Seek (0, SeekOrigin.Begin);
Larry Smith
2
To działało tylko dla mnie po zmianie AuthorizeAttributena Attribute(w ASP.Net Core 3.1).
Sigmund
Chłopaki, proszę, upewnijcie się, że dodaliście wspomniane biblioteki. Miałem już kod, ale EnableBuffering pokazywał czerwoną falistą linię, dopóki nie zorientowałem się, że brakuje odwołania do Microsoft.AspNetCore.Http. Dzięki Androidowi!
kaarthick raman
18

Aby móc cofnąć treść żądania, odpowiedź @ Jean pomogła mi znaleźć rozwiązanie, które wydaje się działać dobrze. Obecnie używam tego dla oprogramowania pośredniczącego Global Exception Handler, ale zasada jest taka sama.

Stworzyłem oprogramowanie pośredniczące, które w zasadzie umożliwia przewijanie w treści żądania (zamiast dekoratora).

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

Można to następnie wykorzystać w swoim Startup.cs:

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

Korzystając z tego podejścia, udało mi się pomyślnie przewinąć strumień treści żądania.

SaoBiz
źródło
1
To działało świetnie dla mnie @SaoBiz, dziękuję! jedna literówka, zmiana 2-ty to do budowniczy w UseEnableRequestRewind(this IApplicationBuilder builder).
Richard Logwood
@RichardLogwood Cieszę się, że pomogło! Dzięki za znalezienie literówki! Naprawiony. :)
SaoBiz
7

To trochę stary wątek, ale odkąd tu dotarłem, pomyślałem, że opublikuję moje odkrycia, aby mogli pomóc innym.

Najpierw miałem ten sam problem, w którym chciałem pobrać Request.Body i coś z tym zrobić (logowanie / audyt). Ale poza tym chciałem, żeby punkt końcowy wyglądał tak samo.

Wydawało się więc, że wywołanie EnableBuffering () może załatwić sprawę. Następnie możesz wykonać wyszukiwanie (0, xxx) na ciele i ponownie przeczytać zawartość itp.

Doprowadziło to jednak do mojego następnego wydania. Otrzymywałbym wyjątki „Synchornous operacje są zabronione” podczas uzyskiwania dostępu do punktu końcowego. Tak więc obejście polega na ustawieniu właściwości AllowSynchronousIO = true w opcjach. Można to osiągnąć na wiele sposobów (ale nie ma znaczenia, aby tutaj szczegółowo opisywać).

WTEDY następną kwestią jest to, że kiedy idę przeczytać Wniosek, Ciało zostało już usunięte. Fuj. Więc co daje?

Używam Newtonsoft.JSON jako mojego parsera [FromBody] w wywołaniu endpiont. To właśnie jest odpowiedzialne za synchroniczne odczyty, a także zamyka strumień, gdy jest zakończony. Rozwiązanie? Odczytać strumień, zanim przejdzie do analizy JSON? Jasne, to działa i skończyło się na tym:

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

Więc teraz mogę uzyskać dostęp do treści przy użyciu HttpContext.Items ["request_body"] w punktach końcowych, które mają atrybut [ReadRequestBodyIntoItems].

Ale człowieku, wydaje się, że to zbyt wiele obręczy, aby przez nie przeskoczyć. Więc tutaj skończyłem i jestem z tego naprawdę zadowolony.

Mój punkt końcowy zaczął się jako coś takiego:

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

Ale o wiele łatwiej jest po prostu zmienić podpis, na przykład:

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

Bardzo mi się to podobało, ponieważ odczytuje strumień treści tylko raz i mam kontrolę nad deserializacją. Jasne, fajnie, że ASP.NET core robi za mnie tę magię, ale tutaj nie tracę czasu na dwukrotne czytanie strumienia (być może buforowanie za każdym razem), a kod jest dość przejrzysty i czysty.

Jeśli potrzebujesz tej funkcji w wielu punktach końcowych, być może podejście do oprogramowania pośredniczącego może być czystsze lub możesz przynajmniej hermetyzować wyodrębnianie treści do funkcji rozszerzającej, aby kod był bardziej zwięzły.

W każdym razie nie znalazłem żadnego źródła, które poruszyłoby wszystkie 3 aspekty tego zagadnienia, stąd ten post. Mam nadzieję, że to komuś pomoże!

BTW: używano ASP .NET Core 3.1.

Shane Anderson
źródło
Jeśli program nie może przeanalizować ciągu JSON do NyObjectType, nie mogę odczytać wartości z „request_body”
Ericyu67,
3

Niedawno natknąłem się na bardzo eleganckie rozwiązanie, które pobiera losowy JSON, że nie masz pojęcia o strukturze:

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

Po prostu takie proste.

naspiński
źródło
Dziękuję, to naprawdę działa. Użyłem GetRawText()metody JsonElement i otrzymałem tekst JSON.
mrNone
Nie da ci to rzeczywistej treści żądania, jeśli DTO wykonuje jakieś przetwarzanie podczas jego tworzenia, takie jak ustawienie wartości domyślnych lub coś takiego.
Ebram Shehata
3

Szybkim sposobem dodania buforowania odpowiedzi w .NET Core 3.1 jest

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

w Startup.cs. Zauważyłem, że to również gwarantuje, że buforowanie zostanie włączone przed odczytaniem strumienia, co stanowiło problem dla .Net Core 3.1 z niektórymi innymi odpowiedziami pośredniczącymi / filtrami autoryzacji, które widziałem.

Następnie możesz przeczytać treść żądania za pośrednictwem HttpContext.Request.Bodyswojego programu obsługi, jak sugerowało to kilka innych osób.

Warto również wziąć pod uwagę, że EnableBufferingma przeciążenia, które pozwalają ograniczyć ilość buforowanej pamięci, zanim użyje pliku tymczasowego, a także ogólny limit bufora. Uwaga: jeśli żądanie przekroczy ten limit, zostanie zgłoszony wyjątek, a żądanie nigdy nie dotrze do osoby obsługującej.

Stephen Wilkinson
źródło
U mnie to zadziałało znakomicie (3.1). Zacytowałem Cię na inne pytanie: stackoverflow.com/a/63525694/6210068
Lance
Pracowano nad 3.1. Również przypomnienie dla użytkowników, którzy będą z tego korzystać: upewnij się, że umieścisz go we właściwej kolejności w Startup.cs.
Ebram Shehata
2

Miałem podobny problem podczas korzystania z ASP.NET Core 2.1:

  • Potrzebuję niestandardowego oprogramowania pośredniczącego, aby odczytać przesłane dane i przeprowadzić kilka testów bezpieczeństwa
  • użycie filtru autoryzacji jest niepraktyczne ze względu na dużą liczbę działań, których dotyczy
  • Muszę zezwolić na powiązanie obiektów w akcjach ([FromBody] someObject). Dziękuję SaoBizza wskazanie tego rozwiązania.

Tak więc oczywistym rozwiązaniem jest umożliwienie przewijania żądania, ale upewnij się, że po przeczytaniu treści powiązanie nadal działa.

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

(umieść to na początku metody Configure)

app.UseMiddleware<EnableRequestRewindMiddleware>();

Inne oprogramowanie pośredniczące

Jest to część oprogramowania pośredniego, która wymaga rozpakowania informacji POST w celu sprawdzenia rzeczy.

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
źródło
2

do odczytu Body, możesz czytać asynchronicznie.

użyj następującej asyncmetody:

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

Przetestuj z listonoszem:

wprowadź opis obrazu tutaj

Działa dobrze i przetestowany w Asp.net corewersji 2.0 , 2.1 , 2.2, 3.0.

Mam nadzieję, że jest przydatny.

AminRostami
źródło
1

Ta IHttpContextAccessormetoda działa, jeśli chcesz jechać tą trasą.

TLDR;

  • Wstrzyknąć IHttpContextAccessor

  • Przewijanie do tyłu -- HttpContextAccessor.HttpContext.Request.Body.Seek(0, System.IO.SeekOrigin.Begin);

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

Więcej - próba zwięzłego, niekompilującego się przykładu elementów, które musisz upewnić się, że są na miejscu, aby uzyskać użyteczne IHttpContextAccessor. Odpowiedzi wskazały poprawnie, że podczas próby odczytania treści żądania należy cofnąć się do początku. CanSeek, PositionWłaściwości na organizm żądanie strumień pomocne dla sprawdzenia tego.

.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
źródło
1

Udało mi się odczytać treść żądania w aplikacji asp.net core 3.1, takiej jak ta (razem z prostym oprogramowaniem pośredniczącym, które umożliwia buforowanie-włączanie przewijania, wydaje się działać we wcześniejszych wersjach .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
źródło
0

Chciałem również przeczytać Request.Body bez automatycznego mapowania go na jakiś model parametrów akcji. Przetestowałem wiele różnych sposobów, zanim to rozwiązałem. I nie znalazłem tutaj żadnego działającego rozwiązania. To rozwiązanie jest obecnie oparte na platformie .NET Core 3.0.

reader.readToEnd () zszywał się jak prosty sposób, mimo że został skompilowany, wyrzucił wyjątek w czasie wykonywania, wymagający ode mnie użycia wywołania asynchronicznego. Zamiast tego użyłem ReadToEndAsync (), jednak czasami działało, a czasami nie. Daje mi błędy typu, nie można odczytać po zamknięciu strumienia. Problem polega na tym, że nie możemy zagwarantować, że zwróci wynik w tym samym wątku (nawet jeśli używamy await). Potrzebujemy więc oddzwonienia. To rozwiązanie zadziałało dla mnie.

[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
źródło
0

Najprostszy możliwy sposób jest następujący:

  1. W metodzie Controller, z której musisz wyodrębnić treść, dodaj ten parametr: [FromBody] SomeClass value

  2. Zadeklaruj „SomeClass” jako: class SomeClass {public string SomeParameter {get; zestaw; }}

Gdy surowe treści są wysyłane jako json, .net core wie, jak je odczytać bardzo łatwo.

abelabbesnabi
źródło
0

Dla tych, którzy po prostu chcą pobrać treść (treść żądania) z żądania:

Użyj [FromBody]atrybutu w parametrze metody kontrolera.

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

Jak mówi dokument: ten atrybut określa, że ​​parametr lub właściwość powinny być powiązane za pomocą treści żądania.

sirazal
źródło