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.
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
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 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.
Comments for this article