3
votes

I consider myself pretty good in C#, but I am facing trouble in understanding the following piece of code:

using (var memoryStream = new MemoryStream())
{
    var responseStream = httpContext.Response.Body;
    httpContext.Response.Body = memoryStream;

    await this.next(httpContext);

    using (var compressedStream = new GZipStream(responseStream, CompressionLevel.Optimal))
    {
        httpContext.Response.Headers.Add("Content-Encoding", new [] { "gzip" });
        memoryStream.Seek(0, SeekOrigin.Begin);
        await memoryStream.CopyToAsync(compressedStream);
    }
}

This code is extracted from an ASP.Net Core middleware that compresses the HTTP response, and "surprisingly", it works... or so it seems (I tested it with Fiddler).

Let me put my understanding first:

  • The code starts with taking a reference to httpContext.Response.Body in responseStream.
  • Then it replaces httpContext.Response.Body reference with the newly initialised memoryStream.
  • If my understanding of how C# references work, I say we still have a reference to the original httpContext.Response.Body data with responseStream, while httpContext.Response.Body new data is empty.
  • Next, we are calling the next middleware in the pipeline.
  • Because this.next() is awaitable, our code execution will "stop" until all middlewares return.
  • When our code execution "resumes", it will initialise a GZipStream, adds a response header, and "seeks" to the beginning of memoryStream.
  • Finally, it copies the content or memoryStream to compressedStream, which writes it to responseStream.

So, what is the relation between memoryStream, compressedStream, and responseStream? We created compressedStream to write to responseStream and then eventually to httpContext.Response.Body, but the reference from responseStream to httpContext.Response.Body isn't there anymore?

2

2 Answers

2
votes

FWIW the OOB ResponseCompressionMiddleware looks a bit different nowadays.

But in the sample you pasted, i'll annotate to illustrate why memoryStream is NOT actually empty by the time it gets copied to compressedStream.

using (var memoryStream = new MemoryStream()) // Create a buffer so we can capture response content written by future middleware/controller actions
{
    var responseStream = httpContext.Response.Body; // save a reference to the ACTUAL STREAM THAT WRITES TO THE HTTP RESPONSE WHEN WRITTEN TO.
    httpContext.Response.Body = memoryStream; // replace HttpContext.Response.Body with our buffer (memoryStream).

    await this.next(httpContext); // <== somewhere in here is where HttpContext.Response.Body gets written to, and since Body now points to memoryStream, really memoryStream gets written to.

    using (var compressedStream = new GZipStream(responseStream, CompressionLevel.Optimal)) // Here is where we wrap the ACTUAL HTTP RESPONSE STREAM with our ready-to-write-to compressedStream.
    {
        httpContext.Response.Headers.Add("Content-Encoding", new [] { "gzip" });
        memoryStream.Seek(0, SeekOrigin.Begin); // Previously, we set HttpContext.Response.Body to a memoryStream buffer, then SOMEONE in the middleware chain or controller action wrote to that Body, we can seek to the beginning of what they wrote, and copy that content to the compressedStream.
        await memoryStream.CopyToAsync(compressedStream);
    }
}

Hope that helps.

0
votes

The code simply takes what the next middleware in the pipeline has written to the memoryStream object and compresses it for responding back to the client with whatever was in the responseStream before this piece of middleware ran.

So existing responseStream content + memoryStream content.