0
  • Background: I need to relay the content of the request to multiple other servers (via client.SendAsync(request)).
  • Problem: After first request the content stream is empty
[HttpPost]
public async Task<IActionResult> PostAsync() {

    for (var n = 0; n <= 1; n++) {
        using (var stream = new MemoryStream()) {
            await Request.Body.CopyToAsync(stream);
            // why is stream.length == 0 in the second iteration? 
        }
    } 

    return StatusCode((int)HttpStatusCode.OK);
}
jwillmer
  • 2,962
  • 2
  • 33
  • 65

2 Answers2

2

Streams have a pointer indicating at which position the stream is; after copying it, the pointer is at the end. You need to rewind a stream by setting its position to 0.

This is however only supported in streams that support seeking. You can read the request stream only once. This is because it's read "from the wire", and therefore doesn't support seeking.

When you want to copy the request stream to multiple output streams, you have two options:

  • Forward while you read
  • Read once into memory, then forward at will

The first option means all forwards happen at the same speed; the entire transfer goes as slow as the input, or as slow as the slowest reader. You read a chunk from the caller, and forward that chunk to all forward addresses.

For the second approach, you'll want to evaluate whether you can hold the entire request body plus the body for each forward address in memory. If that's not expected to be a problem and properly configured with sensible limits, then simply copy the request stream to a single MemoryStream and copy and rewind that one after every call:

using (var bodyStream = new MemoryStream()) 
{
    await Request.Body.CopyToAsync(bodyStream);

    for (...) 
    {
        using (var stream = new MemoryStream()) 
        {
            await bodyStream.CopyToAsync(stream);
            // Rewind for next copy
            bodyStream.Position = 0;
        }
    }
}
CodeCaster
  • 131,656
  • 19
  • 190
  • 236
  • 1
    I came up with the same solution but I was missing the reason for this behavior. Thank you for the explanation :) – jwillmer Mar 20 '20 at 14:08
1

I found out that the CopyToAsync function sets the origin stream position to the last read position. The next time I use CopyToAsync the stream starts reading from the last read position and does not find more content. However I could not use Request.Body.Position = 0 since it is not supported. I ended up copying the stream once more and reset the position after each copy.

If someone knows a cleaner solution you are welcome to point it out.

using (var contentStream = new MemoryStream()) {
    await Request.Body.CopyToAsync(contentStream);                 

    for (var n = 0; n <= 1; n++) {
        using (var stream = new MemoryStream()) {
            contentStream.Position = 0;
            await contentStream.CopyToAsync(stream);

            // works
        }
    }
}
jwillmer
  • 2,962
  • 2
  • 33
  • 65