A388

ASP.NET Core Middleware for HTML Minification

Today I was reading a blog post by Jeremy Lindsay about an ASP.NET Core middleware to prettify HTML output. It piqued my interest because I use a similar middleware in my projects to minify HTML. Looking through his code and comparing it to mine, they're very similar with maybe the biggest difference being that he uses AngleSharp and I use HtmlAgilityPack.

I have not tested his solution, but I have no doubt it works. My only concern is the use of the inner StreamReader, but it may be because of AngleSharp input requirements. I haven't used AngleSharp either so I can't say.

I do know that two things that bit me in the butt with my minifier were not applying the correct encoding when loading the source HTML, and not filtering out non-text/html responses.

Arex388.AspNetCore NuGet Package

I was using my minifier across multiple projects, so I ended up recently extracting it out into a NuGet package with other ASP.NET Core extensions I use. The NuGet package is called Arex388.AspNetCore, and the source code is available on GitHub.

For those interested in only the code, as it is at the time, I write this, here it is:

HtmlMinifierMiddleware
public sealed class HtmlMinifierMiddleware {
	private RequestDelegate Next { get; }

	public HtmlMinifierMiddleware(
		RequestDelegate next) => Next = next;

	public async Task InvokeAsync(
		HttpContext context) {
		var response = context.Response;
		var stream = response.Body;

		using (var memoryStream = new MemoryStream()) {
			response.Body = memoryStream;

			await Next(context);

			memoryStream.Seek(0, SeekOrigin.Begin);

			if (response.ContentType.Contains("text/html")) {
				var document = new HtmlDocument();

				document.Load(memoryStream, Encoding.UTF8);
				document.DocumentNode.TrimWhitespace();
				document.Save(stream);
			}

			await memoryStream.CopyToAsync(stream);

			response.Body = stream;
		}
	}
}
HtmlNodeExtensions
public static class HtmlNodeExtensions {
	public static IList<HtmlNode> SelectNodesAsList(
		this HtmlNode node,
		string xpath) {
		return node.SelectNodes(xpath)?.ToList() ?? new List<HtmlNode>(0);
	}

	public static void TrimWhitespace(
		this HtmlNode document) {
		var textNodes = document.SelectNodesAsList("//text()").Where(
			n => string.IsNullOrWhiteSpace(n.InnerHtml));

		foreach (var textNode in textNodes) {
			textNode.Remove();
		}

		var commentNodes = document.SelectNodesAsList("//comment()").Where(
			n => n.InnerHtml != "<!DOCTYPE html>");

		foreach (var commentNode in commentNodes) {
			commentNode.Remove();
		}
	}
}
HtmlMinifierMiddlewareExtensions
public static class HtmlMinifierMiddlewareExtensions {
	public static IApplicationBuilder UseHtmlMinifierMiddleware(
		this IApplicationBuilder builder) {
		return builder.UseMiddleware<HtmlMinifierMiddleware>();
	}
}

I usually add it to the Startup class right above the app.UseMvc(... registration. I'll write a follow up post about why I do HTML minification.