Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

I have trouble understanding a GZipStream ASP.Net Core Middleware

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?

like image 391
CodeAddict Avatar asked Jan 31 '26 11:01

CodeAddict


1 Answers

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.

like image 81
AndyCunningham Avatar answered Feb 02 '26 23:02

AndyCunningham