Як прочитати тіло запиту в основному контролері webapi asp.net?


104

Я намагаюся прочитати тіло запиту в OnActionExecutingметоді, але завжди отримую nullтело.

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

Я намагався явно встановити положення потоку в 0, але це також не спрацювало. Оскільки це ASP.NET CORE, на мою думку, справи йдуть трохи інакше. Тут я бачу всі зразки, що стосуються старих версій webapi.
Чи є інший спосіб зробити це?


4
Будьте обережні, якщо тіло запиту було прочитане вже раніше під час конвеєру запитів, воно пусте, коли ви намагаєтесь прочитати його вдруге
Фабіо

1
Можливий дублікат Read HttpContent у контролері WebApi
Фабіо

@Fabio Дякую за інформацію, чи можемо ми встановити позицію і прочитати її ще раз?
Касун Косватта

@KasunKoswattha - За задумом вміст тіла розглядається як прямий потік, який можна прочитати лише один раз.
user270576

Думаю, питання скоріше націлено на фільтри чи проміжне програмне забезпечення, ніж на контролери.
Джим Ахо,

Відповіді:


111

У ASP.Net Core здається складним читати кілька разів запит тіла, однак, якщо ваша перша спроба робить це правильно, з наступними спробами ви повинні бути добре.

Я прочитав кілька поворотів, наприклад, замінивши потік тіла, але я думаю, що наступне є найчистішим:

Найважливішими моментами є

  1. щоб повідомити запит, що ви прочитаєте його текст двічі або більше разів,
  2. не закривати потік тіла, і
  3. повернути його в початкове положення, щоб внутрішній процес не загубився.

[РЕДАГУВАТИ]

Як вказував Мурад, ви також можете скористатися розширенням .Net Core 2.1: EnableBufferingвоно зберігає великі запити на диск, а не зберігає їх у пам'яті, уникаючи проблем великих потоків, що зберігаються в пам'яті (файли, зображення, ...) . Ви можете змінити тимчасову папку, встановивши ASPNETCORE_TEMPзмінну середовища, а файли видаляються після закінчення запиту.

У AuthorizationFilter ви можете зробити наступне:

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

Потім ви можете знову використовувати тіло в обробнику запитів.

У вашому випадку, якщо ви отримаєте нульовий результат, це, мабуть, означає, що тіло вже було прочитане на більш ранньому етапі. У цьому випадку вам може знадобитися використовувати проміжне програмне забезпечення (див. Нижче).

Однак будьте обережні, якщо обробляєте великі потоки, оскільки поведінка передбачає, що все завантажується в пам’ять, це не повинно спрацьовувати у разі завантаження файлу.

Можливо, ви захочете використовувати це як проміжне програмне забезпечення

Мій виглядає так (знову ж таки, якщо ви завантажуєте / завантажуєте великі файли, це слід вимкнути, щоб уникнути проблем із пам'яттю):

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

}

потік все ще порожній, навіть якщо я перемотую назад у Позицію 0.
Адріан Насуї

2
Ви використовували req.EnableRewind();? Я використовую код вище, і він працює добре.
Жан

використовували req.EnableRewind (); не працює. Я отримую Position = 0, довжина тіла = 26, але при читанні потоку 'body' виникає порожній рядок.
Адріан Насуї

Ви повинні дозволити перемотування назад до першого читання тіла, а не до 2-го, якщо цього не сталося, я думаю, це не спрацює
Жан

3
Можна також використовувати request.EnableBuffering()(обгортку EnableRewind()). Він доступний у ASP.NET Core 2.1 docs.microsoft.com/en-us/dotnet/api/…
Мурад,

27

Більш чітке рішення працює в ASP.Net Core 2.1 / 3.1

Клас фільтра

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

У контролері

[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
Для netcore3.0 це було б .EnableBuffering () замість.EnableRewind()
mr5

Спасибі @ mr5 - Оновлено мою відповідь
Andriod,

Я знайшов це під час виправлення деяких оновлень .net Core 2.2 -> Core 3.1, які порушили спосіб EnableRewind (). Я думаю, що для цього потрібен ще один рядок коду, без якого я не зміг би перечитати Body: HttpContext.Request.Body.Seek (0, SeekOrigin.Begin);
Ларрі Сміт,

2
Це спрацювало у мене лише після зміни AuthorizeAttributeна Attribute(в ASP.Net Core 3.1).
Зигмунд

Хлопці, не забудьте додати згадані бібліотеки. Я вже мав код, але EnableBuffering показував червону кривизну, поки я не зрозумів, що посилання Microsoft.AspNetCore.Http відсутнє. Завдяки Android!
kaarthick raman

18

Щоб мати змогу перемотати назад тіло запиту, відповідь @ Jean допомогла мені знайти рішення, яке, здається, добре працює. В даний час я використовую це для проміжного програмного забезпечення для обробки винятків, але принцип той же.

Я створив проміжне програмне забезпечення, яке в основному дозволяє перемотувати тіло запиту (замість декоратора).

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

Потім це можна використовувати у вашому Startup.csподібному:

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

Використовуючи цей підхід, я зміг успішно перемотати потік основного запиту.


1
Це мені чудово вдалось @SaoBiz дякую! одна помилка, зміна другого це для будівельник в UseEnableRequestRewind(this IApplicationBuilder builder).
Річард Логвуд,

@RichardLogwood Радий, що допомогло! Дякуємо, що знайшли друкарську помилку! Виправлено. :)
SaoBiz

6

Це трохи стара тема, але, оскільки я сюди потрапив, я вирішив опублікувати свої висновки, щоб вони могли допомогти іншим.

По-перше, у мене була та сама проблема, де я хотів отримати Request.Body і зробити щось із цим (реєстрація / аудит). Але в іншому випадку я хотів, щоб кінцева точка виглядала однаково.

Отже, здавалося, що виклик EnableBuffering () може зробити трюк. Потім ви можете здійснити пошук (0, ххх) на тілі і перечитати вміст тощо.

Однак це призвело до мого наступного випуску. Я отримував би винятки "Синхронні операції заборонені" при доступі до кінцевої точки. Отже, обхідне рішення полягає у встановленні властивості AllowSynchronousIO = true у параметрах. Існує ряд способів досягти цього (але тут не важливо деталізувати ..)

ПОТІМ, наступне питання полягає в тому, що коли я йду читати Запит, він уже вирішений. Тьфу. Отже, що дає?

Я використовую Newtonsoft.JSON як мій парсер [FromBody] у виклику endpiont. Це те, що відповідає за синхронне читання, а також закриває потік, коли це робиться. Рішення? Прочитайте потік, перш ніж він потрапить до аналізу JSON? Звичайно, це працює, і я закінчив з цим:

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

Тож тепер я можу отримати доступ до тіла за допомогою HttpContext.Items ["request_body"] у кінцевих точках, які мають атрибут [ReadRequestBodyIntoItems].

Але людино, це здається занадто багато обручів, щоб проскочити. Отже, ось де я закінчив, і я справді цим задоволений.

Моя кінцева точка починалася приблизно так:

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

Але набагато простіше просто змінити підпис, наприклад:

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

Мені це дуже сподобалось, тому що він читає потік тіла лише один раз, і я контролюю десериалізацію. Звичайно, приємно, якщо ядро ​​ASP.NET робить цю магію для мене, але тут я не витрачаю час на читання потоку двічі (можливо, буферизація кожного разу), і код досить чіткий і чистий.

Якщо вам потрібна ця функціональність на багатьох кінцевих точках, можливо, підходи до проміжного програмного забезпечення можуть бути чистішими, або ви можете принаймні інкапсулювати витяг тіла у функцію розширення, щоб зробити код більш стислим.

У будь-якому випадку, я не знайшов жодного джерела, яке торкалося б усіх 3 аспектів цієї проблеми, отже, і цієї публікації. Сподіваємось, це комусь допомагає!

До речі: це використовувало ASP .NET Core 3.1.


Якщо програма не може проаналізувати рядок JSON на NyObjectType, тоді я не можу прочитати значення з "request_body"
Ericyu67

2

У мене була подібна проблема під час використання ASP.NET Core 2.1:

  • Мені потрібне власне проміжне програмне забезпечення, щоб прочитати розміщені дані та виконати деякі перевірки безпеки щодо них
  • використання авторизаційного фільтра не є практичним через велику кількість дій, на які це впливає
  • Я повинен дозволити об'єкти, що прив'язуються в діях ([FromBody] someObject). Дякуємо SaoBizза те, що вказали на це рішення.

Отже, очевидне рішення полягає в тому, щоб дозволити перезаписуванню запиту, але переконайтесь, що після прочитання основного тексту прив’язка все одно працює.

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

(розмістіть це на початку методу Налаштування)

app.UseMiddleware<EnableRequestRewindMiddleware>();

Деякі інші проміжні програми

Це частина проміжного програмного забезпечення, яке вимагає розпакування розміщеної інформації для перевірки матеріалів.

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

Нещодавно я натрапив на дуже елегантне рішення, в якому беруться випадкові JSON, про структуру яких ви навіть не уявляєте:

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

Так просто.


1

IHttpContextAccessorМетод дійсно працює , якщо ви хочете йти по цьому шляху.

TLDR;

  • Введіть IHttpContextAccessor

  • Перемотування назад 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());

Більше - Спроба стислого, некомпілюючого прикладу предметів, які потрібно переконатись, що вони є на місці, щоб отримати корисні IHttpContextAccessor. Відповіді правильно вказали, що вам потрібно буде повернутися до початку, коли ви спробуєте прочитати тіло запиту. CanSeek, PositionВластивості на тілі запиту потік корисно для перевірки цього.

Документи .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

Мені вдалося прочитати тіло запиту в додатку asp.net core 3.1, як це (разом із простим проміжним програмним забезпеченням, яке дозволяє буферизацію - перемотування назад, схоже, працює для попередніх версій .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

для читання Bodyви можете читати асинхронно.

використовуйте asyncметод наступним чином:

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

Тест з листоношею:

введіть тут опис зображення

Він працює добре і перевірений у Asp.net coreверсії 2.0 , 2.1 , 2.2, 3.0.

Я сподіваюся, це корисно.


0

Я також хотів прочитати Request.Body без автоматичного відображення його до якоїсь моделі параметрів дії. Перевірив багато різних способів, перш ніж це вирішити. І я не знайшов жодного робочого рішення, описаного тут. На даний момент це рішення базується на середовищі .NET Core 3.0.

reader.readToEnd () видавався простим способом, хоча він і скомпілювався, він викинув виняток часу виконання, який вимагав від мене використання асинхронного виклику. Тому натомість я використовував ReadToEndAsync (), однак це іноді спрацьовувало, а іноді ні. Видаючи мені помилки типу, не можу прочитати після закриття потоку. Проблема в тому, що ми не можемо гарантувати, що це поверне результат у тому ж потоці (навіть якщо ми використовуємо await). Тож нам потрібен якийсь зворотний дзвінок. Це рішення спрацювало для мене.

[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

Найпростіший можливий спосіб зробити це наступним:

  1. У методі контролера, з якого потрібно витягти тіло, додайте цей параметр: [FromBody] Значення SomeClass

  2. Оголосіть "SomeClass" як: class SomeClass {public string SomeParameter {get; встановити; }}

Коли вихідне тіло надсилається як json, .net core знає, як його легко прочитати.


0

Тим, хто просто хоче отримати вміст (тіло запиту) із запиту:

Використовуйте [FromBody]атрибут у параметрі методу контролера.

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

Як сказано в документі: цей атрибут вказує, що параметр або властивість повинні бути пов'язані за допомогою тіла запиту.


0

Швидким способом додати буферизацію відповідей у ​​.NET Core 3.1 є

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

у Startup.cs. Я виявив, що це також гарантує, що буферизація буде ввімкнена до того, як потік буде прочитаний, що було проблемою для .Net Core 3.1 з деякими іншими відповідями проміжного програмного забезпечення / авторизації, які я бачив.

Тоді ви можете прочитати тіло вашого запиту HttpContext.Request.Bodyу своєму обробнику, як пропонували деякі інші.

Також варто врахувати, що EnableBufferingє перевантаження, які дозволяють обмежити обсяг буферизації в пам'яті перед тим, як використовувати тимчасовий файл, а також загальний ліміт для вашого буфера. Примітка. Якщо запит перевищує це обмеження, буде видано виняток, і запит ніколи не надійде до вашого обробника.


Для мене це чудово спрацювало (3.1). Цитував вас з іншого питання: stackoverflow.com/a/63525694/6210068
Ленс,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.