Search code examples
c#asp.net.netprotobuf-netsystem.io.pipelines

How to read all POST body bytes in an ASP.NET app using System.IO.Pipelines.PipeReader?


I am trying to switch my ASP.NET application using .Net 6 from Stream to PipeReader as recommended by Microsoft. Here is my custom method:

private static async Task<byte[]> GetRequestBodyBytesAsync(PipeReader reader)
{
    byte[] bodyBytes;

    do
    {
        ReadResult readResult = await reader.ReadAsync();
        if (readResult.IsCompleted || readResult.IsCanceled)
        {
            ReadOnlySequence<byte> slice = readResult.Buffer.Slice(readResult.Buffer.Start, readResult.Buffer.End);
            bodyBytes = BuffersExtensions.ToArray(slice);
            reader.AdvanceTo(readResult.Buffer.End);
            break;
        }
    } while (true);

    return bodyBytes;
}

However when I use the above static method in my controller:

[HttpPost]
[Route(MyUrl)]
public async Task MyPostAsync()
{
    byte[] bodyBytes = await GetRequestBodyBytesAsync(Request.BodyReader);
    MyProtobuf myProtobuf = MyProtobuf.Parser.ParseFrom(bodyBytes);

then I can see in the debugger that the readResult.IsCompleted is never true and the readResult.Buffer stays the same 21 bytes.

Adding else { reader.AdvanceTo(readResult.Buffer.Start); } has not changed anything.

I need the complete byte array, so that I can pass it to Protobuf parsing method (there are no streaming parsers for protobuf)... how can I please use IO.Pipelines here?

UPDATE:

The following method (found in PipeReaderExtensions.cs and confirmed by Marc) works now for me and it even can be improved further by avoiding copying data around - see the great comments by Marc below.

private static async Task<byte[]> GetRequestBodyBytesAsync(PipeReader reader)
{
    do
    {
        ReadResult readResult = await reader.ReadAsync();
        if (readResult.IsCompleted || readResult.IsCanceled)
        {
            return readResult.Buffer.ToArray();
        }

        // consume nothing, keep reading from the pipe reader until all data is there
        reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End);
    } while (true);
}

Solution

  • You are meant to call AdvanceTo after every ReadAsync, so you should really have an else that does something like:

    reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End);
    

    This says "I've checked everything, and consumed nothing" (at least semantically; it doesn't matter that you haven't really looked at anything - the point is to say "all of these bytes: aren't useful to me yet") - which means that ReadAsync now shouldn't try to give you anything else until it has some additional data or the data has ended. I'm a little surprised that it didn't throw an exception already, actually.