Mark Ashley Bell

25 Jul 2021

Reading the raw request body as a string in ASP.NET Core

ASP.NET MVC model binding is great, but occasionally you just need to access the body of a request as a raw string within a controller method.

ASP.NET MVC 5 (and probably some previous versions)

In the .NET Framework version of MVC, this is simple. You first reset the position of the stream, then re-read it:

Request.InputStream.Position = 0;

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

The stream positon reset is needed because the MVC framework will already have read the stream content, in order to use it internally. Without it, you just read zero bytes and hence get an empty string.

ASP.NET Core 3+

In Core MVC, it seems things are significantly more complicated.

The same "empty string" problem occurs when you read from the stream without resetting it, so we need to do that. There is no Request.InputStream now, as Request.Body is itself a Stream, but let's try a direct translation:

Request.Body.Position = 0;

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

That looks fine, and compiles... but throws a NotSupportedException at runtime.

In fact, trying to reset the stream position by any of the standard methods will result in a NotSupportedException being thrown.

So the first part of the puzzle—not being able to reset the stream position—is solved like this:

Request.EnableBuffering();

Request.Body.Position = 0;

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

Request.EnableBuffering() just calls the internal BufferingHelper.EnableRewind() method, which replaces the request body with a seekable stream and correctly registers it for disposal/cleanup by the framework.

However, this code still doesn't work, throwing an InvalidOperationException at runtime, with a Synchronous operations are disallowed message.

So, you need to call the asynchronous read method of StreamReader and await the result:

Request.EnableBuffering();

Request.Body.Position = 0;

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

Now everything works, giving us a string containing all the body content.

I've wrapped this up into a helper extension, with one extra tweak: resetting the Body stream position back to 0 after the read (it's only polite):

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

Now I can call this in a controller action to get the raw body string, while also still having access to any bound models and/or the Request.Form collection.

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

    // Other code here

    return Ok();
}

Considering how simple it sounds, this was surprisingly tricky, so I hope this little write-up helps you out if you're having the same problem.

Addendum: Model Binding

After I originally published this article, a fellow dev named Damian emailed to let me know that this didn't work in a .NET 5 project with model binding.

I thought this might be a change in behaviour between .NET Core 3.1 and .NET 5, but it turns out this is actually the case under both versions.

Calling request.EnableBuffering() (either directly or via my extension method) within a controller action won't work if you also need model binding, e.g:

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

    // rawRequestBody will be *empty* here

    return Ok();
}

In this case, the MVC model binder will fully consume the body stream, so reading it after that just returns an empty string.

What we need to do here is call EnableBuffering() before the request reaches the MVC pipeline, so that the body stream is still available after the model binder has read from it.

We can do this in a number of ways, all of which involve middleware. All of these solutions are called from within the Configure method of the Startup class—you can see them in context here.

Inline Middleware

This is the simplest solution, and may be best for you if you just want to enable this behaviour for all requests.

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

Custom Middleware

It's arguably cleaner to encapsulate the behaviour in a custom middleware class, e.g:

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

Then within Configure:

app.UseMiddleware<EnableRequestBodyBufferingMiddleware>();

Conditional Middleware Application

This is the "best" solution (YMMV), in that the middleware can be applied only to actions which require it:

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

There is now a bare-bones example solution here which you can clone and use to help figure all of this out. Happy coding!

Originally published on 20 Mar 2021; updated on 25 Jul 2021

Questions or comments? Get in touch @markeebee, or email [Turn On Javascript To View Email Address].

More articles

© Mark Ashley Bell 2023