Minify ASP.NET MVC Core response using custom middleware and pipeline

Html response content minification on the fly in ASP.NET Core

For my website, since it runs on Umbraco 6 and it is basically ASP.NET MVC application. To reduce load time and increase website performances I decided to alternate the response of the application and minify it using simple regular expression.

Initially I was trying to do this with YUI Compressor library which has .NET port as NuGet YUICompressor.NET which works fine in most of the cases, but in my case does not skip PRE tags which I use for code snippets on the website. Clearly this solution did not work for me, so as I mentioned I relied on HttpModule and RegEx to make this work. You can find the details explained in Minify HTML output of your pages article.

Note

Alternatively you can use WebMarkupMin.AspNetCore NuGet package https://www.nuget.org/packages?q=WebMarkupMin.AspNetCore which is doing this and a lot more out of the box, but since I needed a simple minification with PRE tag skipping it seemed as an overhead to use the whole package for this simple thing which I already heave implemented for ASP.NET MVC as HttpModule

So I made it work with ASP.NET, but when it comes to core you cannot use HttpModule. They are replaced in core with middleware implementations which are added to the pipeline, so it was clear what has to be done - a middleware. It looks simple right, but if you start you will run into two issues which were not so obvious at the first look:

Adding middleware to execute after Mvc middleware is done with generating the response

It is logical to add your middleware after Mvc, once response is generated, but once Mvc middleware executes, it will serve the response and none of the middleware declared in the pipeline after Mvc middleware will not be executed. This can be done with a simple workaround

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {

            app.Use(async (context, next) =>
            {
                //Fetch original response
                Stream responseBody = context.Response.Body;

                // Await next delegate
                await next();

                // Modify response here
            });

            app.UseMvc();
        }
    

This means that in our middleware, we'll await for the next delegate and then do our response modifications. OK this is resolved, but there is one more obstacle in the way to be able to modify the response.

You cannot read and write to Response.Body stream

If you put the brakpoint in your pipeline and run the application, you can peek at the response stream stored in Response.Body

Response Stream Original

You cannot do pretty much anything with this stream, so we will replace it with our own MemoryStream instance and set Response.Body stream to our new MemoryStream instance.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.Use(async (context, next) =>
{
//Fetch original response
Stream responseBody = context.Response.Body;
await next();
using (var newResponseBody = new MemoryStream())
{
context.Response.Body = newResponseBody;
await next();
context.Response.Body = new MemoryStream();
newResponseBody.Seek(0, SeekOrigin.Begin);
context.Response.Body = responseBody;
String html = new StreamReader(newResponseBody).ReadToEnd();
// Update the response HTML here
// Write mified content to response
await context.Response.WriteAsync(html);
                }
            });

            app.UseMvc();
        }
    

On the break point, you can see that our Response.Body stream is available for both reading and writing, so we can do the modifications on it

Response Stream Wrapped

Response minification with RegEx

Since we have our response available and writable, we can apply the Regex expression from article Minify HTML output of your pages and write it to the response

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {

            app.Use(async (context, next) =>
            {
                //Fetch original response
                Stream responseBody = context.Response.Body;

                using (var newResponseBody = new MemoryStream())
                {
                    context.Response.Body = newResponseBody;
                    await next();

                    context.Response.Body = new MemoryStream();

                    newResponseBody.Seek(0, SeekOrigin.Begin);
                    context.Response.Body = responseBody;

                    String html = new StreamReader(newResponseBody).ReadToEnd();
                    
                    // Replace all spaces between tags skipping PRE tags  
                    html = Regex.Replace(html, @"(?<=\s)\s+(?![^<>]*</pre>)", String.Empty);

                    // Replace all new lines between tags skipping PRE tags  
                    html = Regex.Replace(html, "\n(?![^<]*</pre>)", String.Empty);
                   

                    // Write mified content to response
                    await context.Response.WriteAsync(html);
                }
            });

            app.UseMvc();
        }
    

Happy coding!

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform includion ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article