onsdag den 27. maj 2015

A simple usercontrol to make sure resources is refreshed when changed

Often in a project, the things evolve over time, which means both stylesheets and scripts needs to be updated.

A problem you can often run into, is that the client keeps the old version cached, which can break things in really weird ways.

This is a simple way to fix this - all you need to do is replace your script and link tags with this usercontrol.
That way, it ensures the url to the files gets a unique querystring added, which makes sure the client refreshes the file if it gets changed.

To do this, create a new class in a C# project, and call it HashedResourceFile, and paste the following code into it:

public class HashedResourceFile : WebControl
{
    public Uri ResourceUrl { get; set; }

    private string FinalUrl
    {
        get
        {
            string url = ResourceUrl.ToString();

            // Handle external URL's with not-defined protocol.
            if (url.StartsWith("//"))
            {
                return url;
            }

            // If the URL does not start with leading slash, it is external, and we don't do anything with it.
            if (!url.StartsWith("/"))
            {
                return url;
            }

            FileInfo fileInfo = new FileInfo(HttpContext.Current.Server.MapPath(url));

            if (!fileInfo.Exists)
            {
                throw new FileNotFoundException("File in resource not found: " + ResourceUrl);
            }

            string hash;

            using (MD5 md5 = MD5.Create())
            {
                using (FileStream stream = fileInfo.OpenRead())
                {
                    hash = BitConverter.ToString(md5.ComputeHash(stream)).Replace("-", string.Empty);
                }
            }

            return ResourceUrl + "?filehash=" + hash.Substring(0, 8);
        }
    }

    private ResourceLinkType LinkType
    {
        get
        {
            if (ResourceUrl.ToString().EndsWith(".css", StringComparison.InvariantCultureIgnoreCase))
            {
                return ResourceLinkType.Css;
            }

            if (ResourceUrl.ToString().EndsWith(".js", StringComparison.InvariantCultureIgnoreCase))
            {
                return ResourceLinkType.Javascript;
            }

            return ResourceLinkType.Invalid;
        }
    }

    protected override void Render(HtmlTextWriter writer)
    {
        if (writer == null)
        {
            throw new ArgumentNullException("writer", "writer must not be null");
        }

        if (ResourceUrl == null || string.IsNullOrEmpty(ResourceUrl.ToString()))
        {
            return;
        }

        switch (LinkType)
        {
            case ResourceLinkType.Css:
                writer.Write("<link rel=\"stylesheet\" type=\"text/css\" href=\"{0}\" ", FinalUrl);

                foreach (string key in Attributes.Keys.Cast<string>().Where(key => key != "rel" && key != "type" && key != "href"))
                {
                    writer.Write("{0}=\"{1}\" ", key, Attributes[key]);
                }

                writer.Write("/>");
                break;
            case ResourceLinkType.Javascript:
                writer.Write("<script type=\"text/javascript\" src=\"{0}\" ", FinalUrl);

                foreach (string key in Attributes.Keys.Cast<string>().Where(key => key != "type" && key != "src"))
                {
                    writer.Write("{0}=\"{1}\" ", key, Attributes[key]);
                }

                writer.Write("></script>");
                break;
            default:
                return;
        }
    }

    private enum ResourceLinkType
    {
        Invalid,
        Css,
        Javascript
    }
}

Then, when you create your project, add this to the top of the aspx/ascx file:

<%@ Register TagPrefix="test" Namespace="YourNamespace" Assembly="YourDLL" %>

Then, add this instead of your css/js file references:

<test:HashedResourceFile ResourceUrl="/static/css/style.css" runat="server" />

When the page is rendered, it will add a querystring to the file, containing a hash of the file, that changes when the file changes.

Really simple solution, to a really annoying problem :-)