Introduction

To have a web API with compression in ASP.net Core that can be hosted in different servers like Kestrel server or HTTP.sys a middleware is needed since a lot of these servers do not have compression built into them. Microsoft has provided an extension named Microsoft.AspNetCore.ResponseCompression that will add compression support to your APIs but the problem with that extension is that it will compress your static files on the startup of your application, which can have negative affect on the startup time which will affect the performance of your application if hosted in the cloud with load-based scale out hosting.

Another thing to be aware of is that dynamic response compression has some security vulnerabilities(read this article) that convinced me, I only needed to compress the static files to have a faster load time for the client side since most of my API responses are small and will not benefit from the dynamic compression and does not worth risking the security of the application.

Because of these reasons it would make sense to pre compress the static files and just serve them if the user's browser supports the compression type.

Generate compressed static files with Webpack

The application that I was working on had an ASP.net Core web API and a completely separated client react application built with Webpack that consumes the APIs and is bundled with the API application. the API application was also responsible for serving the static files so in reality it was not an API only application. In this section we will have a closer look at the Webpack config file and see how I compressed the static files in the build stage of the client application. As expected, there was an npm package that was doing exactly what I wanted so I just added that to the project. The package is named compression-webpack-plugin and supports most of the famous compression algorithms.

After installing the Webpack plugin, I added the following in the plugin section of the webpack config file. If you are using separate config files for the release build and develop build, I would suggest to only add the compression to the release build so you have a faster development phase since compressing the files take some time and will be annoying in the development and debug phase. It is important to keep the original files, because, if a browser does not support any compression types the server must serve the original files. The two main compression types that I have added was Brotli and Gzip which are the commonly supported compression types in popular browsers and have a good compression ratio and unzipping performance.

plugins: [
  new CompressionPlugin({
    filename: "[path].br[query]",
    algorithm: "brotliCompress",
    test: /\.(js|css|html|svg)$/,
    compressionOptions: { level: 11 },
    threshold: 10240,
    minRatio: 0.8,
    deleteOriginalAssets: false,
  }),
  new CompressionPlugin({
    filename: "[path].gz[query]",
    algorithm: "gzip",
    test: /\.(js|css|html|svg)$/,
    compressionOptions: { level: 9 },
    threshold: 10240,
    minRatio: 0.8,
    deleteOriginalAssets: false,
  }),
];

Serve pre-compressed static files

To serve the files we need to have a physical file provider in the startup Configure function that points to the static files folder, in my example the static files are in a folder named public.
Next, we need to setup the default file option, which in my case is pointing to the index.html file as the default file.

var fileProvider = new PhysicalFileProvider(
  Path.Combine(env.ContentRootPath, "public"));

var options = new DefaultFilesOptions
{
  DefaultFileNames = new List<string> {"index.html"},
  FileProvider = fileProvider
};
app.UseDefaultFiles(options);

Next, we need to install a NuGet package named CompressedStaticFiles that will enable serving the corresponding compressed file instead of the requested file. First, we setup the static file options with the physical file provider we sat up in previous section, then we can use the UseCompressedStaticFiles extension on the application builder.

var preCompressedOptions = new StaticFileOptions
{
  FileProvider = fileProvider,
  RequestPath = new PathString("")
};

app.UseCompressedStaticFiles(preCompressedOptions);

Frontend routing

If your frontend application contains frontend routing, it is important to remember to handle these routes properly in the backend, because if user calls a specific frontend route directly the server needs to serve the default file so the route gets handled afterward in the frontend otherwise the server will return 404 since the route is unknown to the backend. To handle the frontend routes, it is only needed to use a middleware that returns the default file and uses UseStaticFiles extension to serve it.

Note that we cannot use the UseCompressedStaticFiles extension twice because of the way it has been implemented but, in this case, it does not matter as the default file is small enough to be served uncompressed.

Another important note is to be aware that the static files need to be served after the API routes are checked otherwise all routes are counted as frontend routes.

app.Use(async (c, next) =>
{
    c.Request.Path = new PathString("/index.html");
    await next();
}).UseStaticFiles(preCompressedOptions);

Here is the full implementation for serving the pre-compressed static files.

var fileProvider = new PhysicalFileProvider(
  Path.Combine(env.ContentRootPath, "public"));

var options = new DefaultFilesOptions
{
  DefaultFileNames = new List<string> {"index.html"},
  FileProvider = fileProvider
};
app.UseDefaultFiles(options);


var preCompressedOptions = new StaticFileOptions
{
  FileProvider = fileProvider,
  RequestPath = new PathString("")
};

// handles static files, if no static files is returned it means it is a client side route
app.UseCompressedStaticFiles(preCompressedOptions);

//handle client side routes, by rewriting the path and calling the static file provider again
app.Use(async (c, next) =>
{
  c.Request.Path = new PathString("/index.html");
  await next();
}).UseStaticFiles(preCompressedOptions);