Чтение необработанного тела запроса в виде строки в ASP.NET Core

  • Михаил
  • 12 мин. на прочтение
  • 123
  • 25 Nov 2022
  • 25 Nov 2022

Привязка модели ASP.NET MVC великолепна, но иногда вам просто нужно получить доступ к телу запроса в виде необработанной строки в методе контроллера.

ASP.NET MVC 5 (и, возможно, некоторые предыдущие версии)

В версии MVC для .NET Framework это просто. Вы сначала сбрасываете позицию потока, затем перечитываете его:

Request.InputStream.Position = 0;

var rawRequestBody = new StreamReader(Request.InputStream).ReadToEnd();

Сброс позиции потока необходим, потому что среда MVC уже прочитала содержимое потока, чтобы использовать его внутри. Без него вы просто читаете нулевые байты и, следовательно, получаете пустую строку.

ASP.NET Core 3+

В Core MVC кажется, что все значительно сложнее.

Та же проблема с «пустой строкой» возникает, когда вы читаете из потока без его сброса, поэтому нам нужно это сделать. Сейчас нет Request.InputStream, как Request.Bodyи сам a Stream, но попробуем прямой перевод:

Request.Body.Position = 0;

var rawRequestBody = new StreamReader(Request.Body).ReadToEnd();

Это выглядит нормально и компилируется... но выдает ошибку NotSupportedExceptionво время выполнения.

На самом деле, попытка сбросить позицию потока любым из стандартных методов приведет к NotSupportedExceptionброску.

Таким образом, первая часть головоломки — невозможность сбросить позицию потока — решается следующим образом:

Request.EnableBuffering();

Request.Body.Position = 0;

var rawRequestBody = new StreamReader(Request.Body).ReadToEnd();

Request.EnableBuffering()просто вызывает внутренний BufferingHelper.EnableRewind()метод, который заменяет тело запроса доступным для поиска потоком и правильно регистрирует его для удаления/очистки фреймворком.

Однако этот код по- прежнему не работает, выдавая InvalidOperationExceptionво время выполнения Synchronous operations are disallowedсообщение.

Итак, вам нужно вызвать метод асинхронного чтения StreamReaderи awaitрезультат:

Request.EnableBuffering();

Request.Body.Position = 0;

var rawRequestBody = await new StreamReader(request.Body).ReadToEndAsync();

Теперь все работает, давая нам строку, содержащую все содержимое тела.

Я включил это во вспомогательное расширение с одной дополнительной настройкой: сброс Bodyпозиции потока обратно в 0 после чтения (это только вежливо):

public static async Task<string> GetRawBodyAsync(
    this HttpRequest request,
    Encoding encoding = null)
{
    if (!request.Body.CanSeek)
    {
        // We only do this if the stream isn't *already* seekable,
        // as EnableBuffering will create a new stream instance
        // each time it's called
        request.EnableBuffering();
    }

    request.Body.Position = 0;

    var reader = new StreamReader(request.Body, encoding ?? Encoding.UTF8);

    var body = await reader.ReadToEndAsync().ConfigureAwait(false);

    request.Body.Position = 0;

    return body;
}

Теперь я могу вызвать это в действии контроллера, чтобы получить необработанную строку тела, но при этом иметь доступ к любым связанным моделям и/или Request.Formколлекции.

[HttpPost]
public async Task<IActionResult> ExampleAction()
{
    var rawRequestBody = await Request.GetRawBodyAsync();

    // Other code here

    return Ok();
}

Учитывая, как просто это звучит , это было на удивление сложно, поэтому я надеюсь, что эта небольшая статья поможет вам, если у вас возникла та же проблема.

Приложение: Привязка модели

После того, как я впервые опубликовал эту статью, коллега-разработчик по имени Дамиан сообщил мне по электронной почте, что это не работает в проекте .NET 5 с привязкой модели.

Я думал, что это может быть изменением в поведении между .NET Core 3.1 и .NET 5, но оказалось, что это действительно так в обеих версиях.

Вызов request.EnableBuffering()(напрямую или через мой метод расширения) внутри действия контроллера не будет работать, если вам также нужна привязка модели , например:

[HttpPost]
public async Task<IActionResult> ExampleAction(YourViewModel model)
{
    var rawRequestBody = await Request.GetRawBodyAsync();

    // rawRequestBody will be *empty* here

    return Ok();
}

В этом случае связыватель модели MVC полностью использует основной поток, поэтому его чтение после этого просто возвращает пустую строку.

Здесь нам нужно выполнить вызов EnableBuffering() до того, как запрос достигнет конвейера MVC, чтобы основной поток по-прежнему был доступен после того, как модуль связывания модели прочитает его.

Мы можем сделать это несколькими способами, каждый из которых включает промежуточное программное обеспечение. Все эти решения вызываются из Configureметода Startupкласса — вы можете увидеть их в контексте здесь .

Встроенное ПО промежуточного слоя

Это самое простое решение, и оно может быть лучшим для вас, если вы просто хотите включить это поведение для всех запросов.

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

Пользовательское промежуточное ПО

Возможно, чище инкапсулировать поведение в пользовательский класс промежуточного программного обеспечения, например:

public class EnableRequestBodyBufferingMiddleware
{
    private readonly RequestDelegate _next;

    public EnableRequestBodyBufferingMiddleware(RequestDelegate next) =>
        _next = next;

    public async Task InvokeAsync(HttpContext context)
    {
        context.Request.EnableBuffering();

        await _next(context);
    }
}

Затем внутри Configure:

app.UseMiddleware<EnableRequestBodyBufferingMiddleware>();

Условное промежуточное ПО

Это «лучшее» решение ( YMMV ), поскольку промежуточное программное обеспечение можно применять только к тем действиям, которые этого требуют:

app.UseWhen(
    ctx => ctx.Request.Path.StartsWithSegments("/home/withmodelbinding"),
    ab => ab.UseMiddleware<EnableRequestBodyBufferingMiddleware>()
);

Теперь здесь есть простой пример решения, который вы можете клонировать и использовать, чтобы разобраться во всем этом. Удачного кодирования!