Minify and cache static files in ASP.NET Core

This post describes how you can minify and cache static files in ASP.NET Core. You may want to do this in order to make your website faster and to improve in search rankings (SEO).

One important thing to think about before you cache static files is that you need a method to invalidate the cache when your static files changes. Every static file that you save can have a unique name like a GUID or you can append a parameter to the filename that is unique for every new version of the file. You can use a combination of these methods, images can have GUID names and you can use a version parameter for other files.

Bundler and minifier

I use a NuGet package called BuildBundlerMinifier that is created by Mads Kristensen to minify static files in my projects. When you have installed this package, a json file called bundleconfig.json is added to your project in Visual Studio. You can edit this file directly or minfy static files by right-clicking on them in Visual Studio. The contents of the bundleconfig.json file looks like this.

[
  {
    "outputFileName": "wwwroot/css/admin_default.min.css",
    "inputFiles": [ "wwwroot/css/admin_default.css" ]
  },
  {
    "outputFileName": "wwwroot/css/admin_layout.min.css",
    "inputFiles": [ "wwwroot/css/admin_layout.css" ]
  }
]

Invalidate cache

We need a method to invalidate cache for static files. We have a common class in which we have injected IMemoryCache and IHostingEnvironment, we create a reference to a IFileProvider file_provider from the environment variable.

public CommonServices(IMemoryCache cache, IHostingEnvironment environment)
{
    this.cache = cache;
    this.environment = environment;
    this.file_provider = environment.WebRootFileProvider;

} // End of the constructor

Our method to invalidate cache will store the relative path to the static file and an MD5 hash of the file in memory cache. The file provider will watch for changes in the file.

/// <summary>
/// Get a file path with a version hash
/// </summary>
public string GetFilePath(string relative_path)
{
    // Create a variable for a hash
    string hash = "";

    // Get the hash
    if(this.cache.TryGetValue(relative_path, out hash) == false)
    {
        // Create an absolute path
        string absolute_path = this.environment.WebRootPath + relative_path;

        // Make sure that the file exists
        if(File.Exists(absolute_path) == false)
        {
            return relative_path;
        }

        // Create cache options
        MemoryCacheEntryOptions cache_entry_options = new MemoryCacheEntryOptions();

        // Add an expiration token that watches for changes in a file
        cache_entry_options.AddExpirationToken(this.file_provider.Watch(relative_path));
                
        // Create a hash of the file
        using (MD5 md5 = MD5.Create())
        {
            using (Stream stream = File.OpenRead(absolute_path))
            {
                hash = Convert.ToBase64String(md5.ComputeHash(stream));
            }
        }

        // Insert the hash to cache
        this.cache.Set(relative_path, hash, cache_entry_options);
    }

    // Return the url
    return $"{relative_path}?v={hash}";

} // End of the GetFilePath method

Add references to static files

We will use our GetFilePath method when we add references to static files in our layout view, for images it might be better to use a timestamp as version parameter if the names not is unique. Images can be large and many, save the timestamp in your database and append it to the version parameter.

<environment names="Development">
        <link href="@this.tools.GetFilePath(imageUrls.Get("small_icon"))" rel="icon" type="image/x-icon" />
        <link href="@this.tools.GetFilePath("/css/standard_layout.css")" media="screen and (min-width:1344px)" rel="stylesheet" />
        <link href="@this.tools.GetFilePath("/css/medium_layout.css")" media="only screen and (min-width:1024px) and (max-width:1343px)" rel="stylesheet" />
    </environment>
    <environment names="Staging,Production">
        <link href="@this.tools.GetFilePath(imageUrls.Get("small_icon"))" rel="icon" type="image/x-icon" />
        <link href="@this.tools.GetFilePath("/css/standard_layout.min.css")" media="screen and (min-width:1344px)" rel="stylesheet" />
        <link href="@this.tools.GetFilePath("/css/medium_layout.min.css")" media="only screen and (min-width:1024px) and (max-width:1343px)" rel="stylesheet" />
    </environment>

Services

We need to add a service for memory cache in the ConfigureServices method in the StartUp class to be able to cache relative paths and a hash.

// Add memory cache
services.AddDistributedMemoryCache();

We also need to add headers to static files that indicates that they can be cached by a browser and for how long they can be cached. We add this in the Configure method in the StartUp class.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // Use static files
    app.UseStaticFiles(new StaticFileOptions
    {
        OnPrepareResponse = ctx =>
        {
            // Cache static files for 30 days
            ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=2592000");
            ctx.Context.Response.Headers.Append("Expires", DateTime.UtcNow.AddDays(30).ToString("R", CultureInfo.InvariantCulture));
        }
    });

    // More code ...
    
} // End of the Configure method

You can inspect headers for static files in Chrome under Network in Developer tools to make sure that they have correct values.

Leave a Reply

Your email address will not be published. Required fields are marked *