orchard

Orchard Module to Prevent (Indefinite) JS/CSS Caching

A couple of days ago, I saw this tweet

 

A few months ago, I wrote a ‘Resource Timestamper’ Orchard module to add a timestamp to invalidate cache on the client.

Browsers cache resources such as JavaScript and CSS. This is a good thing, but this caching prevents browsers from requesting updated versions of resources. Although doing a ‘hard refresh’ of a page can force a reload of scripts and styles, we can’t expect users to do that to circumvent the problem.

It turns out that resources can be updated using a query string parameter. A client browser considers these as two different requests for the same physical resource:

<link type="text/css" rel="stylesheet" href="somecss.css?1"/>
<link type="text/css" rel="stylesheet" href="somecss.css?2"/>

Using a different query string each time the file is served gets around the caching issue, but loses all benefits of caching.  Better yet, generate a new query string value only when the file has changed.

The Resource Timestamper module modifies resource URLs generated with Orchard’s standard Resource Manager class.  To add a timestamp, call the SetAttribute method with parameters of ‘data-timestamped’, and ‘true’.  Here’s an example from ‘The Theme Machine’s’ Layout.cshtml file:

Style.Include("site.css").SetAttribute("data-timestamped", "true");

On the client, a URL is generated:

http://localhost:30321/OrchardLocal/Themes/TheThemeMachine/styles/site.css?v=635151301095348769

Prior to the timestamp being applied, the URL is converted to an absolute URL.  Why? Well, when writing the module, I realised that trying to append a query string to a relative URL (such as /OrchardLocal/Themes/TheThemeMachine/styles/site.css) didn’t work; the query string was being removed.  I see I’m not the only one who’s noticed Style.Include stripping query strings.

Absolute and relative URLs are often a personal preference, but one issue with absolute URLs is that they must be changed when deploying to a production system, or to another domain.  The plugin uses details of the current request, so there is no need to update the code.

The code generates a timestamp for the resource, and only generates a new timestamp when the file changes in some way.  That way, you get the benefit of caching without the problem of updating the file.

Code Analysis

The single class , FcmResourceManager (Fcm is an abbreviation for my company, Fresh Click Media), derives from Orchard’s ResourceManager class.  To actually use this class, rather than the default ResourceManager, an OrchardSuppressDependency attribute is used:

[OrchardSuppressDependency("Orchard.UI.Resources.ResourceManager")]

The class has a number of Orchard dependencies to get its work done:

private readonly IClock _clock;
private readonly ICacheManager _cacheManager;
private readonly IWebSiteFolder _websiteFolder;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Work<WorkContext> _workContext;

These members are assigned using constructor injection.

The crucial method override is called BuildRequiredResources:

public override IList<ResourceRequiredContext> BuildRequiredResources(string stringResourceType)
{
    var resources = new List<ResourceRequiredContext> base.BuildRequiredResources(stringResourceType));
    foreach (var resource in resources)
    {
        if (resource.Settings.Attributes == null)
            continue;
        if ((from a in resource.Settings.Attributes.ToList()
            where a.Key == "data-timestamped"
            select a).Any())
        {
            resource.Resource.SetUrl(GetCachedUrl(resource));
        }
    }
    return resources;
}

It is this method that is called, and gives us an opportunity to manually set the new URL for the given resource.  This is only done is the ‘data-timestamped’ attribute is found.  In this scenario, the GetCachedUrl method is called.

The GetCachedUrl is shown below:

private string GetCachedUrl(ResourceRequiredContext resource)
{
    if (resource.Resource.Url.StartsWith("http://") || resource.Resource.Url.StartsWith("https://"))
    {
        return resource.Resource.Url;
    }
    string path = GetResourcePath(resource);
    // it's a local file:
    return _cacheManager.Get("Fcm.ResourceCache." + path, ctx =>
    {
        ctx.Monitor(_websiteFolder.WhenPathChanges(path));
        return GetUpdatedUrl(path);
    });
}

The method ignores absolute URLs.  For relative URLs, the path of the file is ascertained.  Orchard’s ICacheManager returns the value of GetUpdatedUrl(…) for a cache key made up of a combination of ‘Fcm.ResourceCache’ and the file’s path.  The cache is invalidated when the path of the file changes.  In this situation the path changes when the file is updated.

The GetUpdatedUrl method takes the original path of the resource, and converts to an absolute URL.

Download and Install

Once you download the module, you can install in the usual way.  The usual way? Go here for more information about installing Orchard modules.

Once the module has been installed, you should see a reassuring message as shown below.  Enable the ‘Resource Timestamper’ feature.

resource timestamper install

One final thing

I hope you find the module useful.  It was written for Orchard 1.5, but has been tested on Orchard 1.7.1 (latest at the time of writing).  It shouldn’t cause you any issues, but you should test it fully on a local environment before deploying it to a production environment. Think that covers it.