Autoversioning CSS/JS in ASP.NET MVC?

China☆狼群 提交于 2019-12-02 16:49:36

The way I solved this problem was to add autoversioning to my MVC project in the AssemblyInfo.cs file like so:

Change
[assembly: AssemblyVersion("1.0.0.0")]
to    
[assembly: AssemblyVersion("1.0.*")]

This means that every time the project is built, it will have a new assembly version which is higher than the previous one. Now you have your unique version number.

I then created an UrlHelperExtension class that will help me get this information when I need it in my views:

public static class UrlHelperExtensions
{
    public static string ContentVersioned(this UrlHelper self, string contentPath)
    {
        string versionedContentPath = contentPath + "?v=" + Assembly.GetAssembly(typeof(UrlHelperExtensions)).GetName().Version.ToString();
        return self.Content(versionedContentPath);
    }
}

You can now easily add a version number to your views in the following manner:

<link href="@Url.ContentVersioned("style.css")" rel="stylesheet" type="text/css" />

When viewing your page source you will now have something that looks like

<link href="style.css?v=1.0.4809.30029" rel="stylesheet" type="text/css" />

1) Use file modification time instead. Here's an example:

public static string GeneratePathWithTime(string cssFileName)
{
  var serverFilePath = server.MapPath("~/static/" + cssFileName);
  var version = File.GetLastWriteTime(serverFilePath).ToString("yyyyMMddhhmmss");
  return string.Format("/static/{0}/{1}", version, cssFileName);
}

This will generate a path like "/static/201109231100/style.css" for "style.css" (assuming the your style.css is located in the static directory). You'll then add a rewrite rule in IIS to rewrite "/static/201109231100/style.css" to "/static/style.css". The version number will only be changed when the css file has been modified and only applies to modified files.

2) You can handle the request to 123.js via an HttpModule and send the latest content of it, but I don't think you can guarantee the request gets the latest version. It depends on how the browser handles its cache. You can set an earlier expiration time (for example, one minute ago) in your response header to tell the browsers to always re-download the file, but it's all up to the browser itself to decide whether to re-download the file or not. That's why we need to generate a different path for our modified files each time we updated our files in your question 1), the browser will always try to download the file if the URL has never been visited before.

Richard Varno

UPDATE: The previous version did not work on Azure, I have simplified and corrected below. (Note, for this to work in development mode with IIS Express, you will need to install URL Rewrite 2.0 from Microsoft http://www.iis.net/downloads/microsoft/url-rewrite - it uses the WebPi installer, make sure to close Visual Studio first)

If you would like to change the actual names of the files, rather than appending a querystring (which is ignored by some proxies / browsers for static files) You can follow the following steps: (I know this is an old post, but I ran across it while developing a solution:

How to do it: Auto-increment the assembly version every time the project is built, and use that number for a routed static file on the specific resources you would like to keep refreshed. (so something.js is included as something.v1234.js with 1234 automatically changing every time the project is built) - I also added some additional functionality to ensure that .min.js files are used in production and regular.js files are used when debugging (I am using WebGrease to automate the minify process) One nice thing about this solution is that it works in local / dev mode as well as production. (I am using Visual Studio 2015 / Net 4.6, but I believe this will work in earlier versions as well.

Step 1: Enable auto-increment on the assembly when built In the AssemblyInfo.cs file (found under the "properties" section of your project change the following lines:

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

to

[assembly: AssemblyVersion("1.0.*")]
//[assembly: AssemblyFileVersion("1.0.0.0")]

Step 2: Set up url rewrite in web.config for files with embedded version slugs (see step 3)

In web.config (the main one for the project) add the following rules in the <system.webServer> section I put it directly after the </httpProtocol> end tag.

<rewrite>
  <rules>
    <rule name="static-autoversion">
      <match url="^(.*)([.]v[0-9]+)([.](js|css))$" />
      <action type="Rewrite" url="{R:1}{R:3}" />
    </rule>
    <rule name="static-autoversion-min">
      <match url="^(.*)([.]v[0-9]+)([.]min[.](js|css))$" />
      <action type="Rewrite" url="{R:1}{R:3}" />
    </rule>
  </rules>
</rewrite>

Step 3: Setup Application Variables to read your current assembly version and create version slugs in your js and css files.

in Global.asax.cs (found in the root of the project) add the following code to protected void Application_Start() (after the Register lines)

            // setup application variables to write versions in razor (including .min extension when not debugging)
            string addMin = ".min";
            if (System.Diagnostics.Debugger.IsAttached) { addMin = ""; }  // don't use minified files when executing locally
            Application["JSVer"] = "v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString().Replace('.','0') + addMin + ".js";
            Application["CSSVer"] = "v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString().Replace('.', '0') + addMin + ".css";

Step 4: Change src links in Razor views using the application variables we set up in Global.asax.cs

@HttpContext.Current.Application["CSSVer"]
@HttpContext.Current.Application["JSVer"]

For example, in my _Layout.cshtml, in my head section, I have the following block of code for stylesheets:

<!-- Load all stylesheets -->
<link rel='stylesheet' href='https://fontastic.s3.amazonaws.com/8NNKTYdfdJLQS3D4kHqhLT/icons.css' />
<link rel='stylesheet' href='/Content/css/main-small.@HttpContext.Current.Application["CSSVer"]' />
<link rel='stylesheet' media='(min-width: 700px)' href='/Content/css/medium.@HttpContext.Current.Application["CSSVer"]' />
<link rel='stylesheet' media='(min-width: 700px)' href='/Content/css/large.@HttpContext.Current.Application["CSSVer"]' />
@RenderSection("PageCSS", required: false)

A couple things to notice here: 1) there is no extension on the file. 2) there is no .min either. Both of these are handled by the code in Global.asax.cs

Likewise, (also in _Layout.cs) in my javascript section: I have the following code:

<script src="~/Scripts/all3bnd100.min.js" type="text/javascript"></script>
<script src="~/Scripts/ui.@HttpContext.Current.Application["JSVer"]" type="text/javascript"></script>
@RenderSection("scripts", required: false)

The first file is a bundle of all my 3rd party libraries I've created manually with WebGrease. If I add or change any of the files in the bundle (which is rare) then I manually rename the file to all3bnd101.min.js, all3bnd102.min.js, etc... This file does not match the rewrite handler, so will remain cached on the client browser until you manually re-bundle / change the name.

The second file is ui.js (which will be written as ui.v12345123.js or ui.v12345123.min.js depending on if you are running in debug mode or not) This will be handled / rewritten. (you can set a breakpoint in Application_OnBeginRequest of Global.asax.cs to watch it work)

Full discussion on this at: Simplified Auto-Versioning of Javascript / CSS in ASP.NET MVC 5 to stop caching issues (works in Azure and Locally) With or Without URL Rewrite (including a way to do it WITHOUT URL Rewrite)

davewasthere

I wrote a Url Helper which does the CacheBusting for me.

public static string CacheBustedContent(this UrlHelper helper, string contentPath)
{
    var path = string.Empty;

    if (helper.RequestContext.HttpContext.Cache["static-resource-" + contentPath] == null)
    {
        var fullpath = helper.RequestContext.HttpContext.Server.MapPath(contentPath);
        var md5 = GetMD5HashFromFile(fullpath);
        path = helper.Content(contentPath) + "?v=" + md5;

        helper.RequestContext.HttpContext.Cache.Add("static-resource-" + contentPath, path, null, System.Web.Caching.Cache.NoAbsoluteExpiration, new TimeSpan(24, 0, 0), System.Web.Caching.CacheItemPriority.Default, null);
    }
    else
    {
        path = helper.RequestContext.HttpContext.Cache["static-resource-" + contentPath].ToString();
    }

    return path;
}

You could replace the GetMD5HashFromFile() with CRC or any other sort of call which generates a unique string based on the contents or last-modified-date of the file.

The downside is this'll get called whenever the cache is invalidated. And if you change the file on live somehow, but don't reset the application pool, you'll probably need to touch the web.config to get it to reload correctly.

You might want to have a look at Dean Hume's Blogpost MVC and the HTML5 Application Cache. In that post, he points out an elegant way of automatically handling versioning per request, using a class library of @ShirtlessKirk:

@Url.Content("~/Content/Site.css").AppendHash(Request)
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!