Autoversioning CSS/JS in ASP.NET MVC? - asp.net-mvc

So I was reading this stackoverflow post about "autoversioning" in ASP.NET MVC for CSS/JS files and was wondering what the "best" strategy is to do this.
The solution provided inserts an assembly number - which means everytime you publish - it will change EVERY SINGLE file which is not ideal because if you make modifications to just 1 *.css or *.js then it will change each and every file.
1) How can it be done just for "single files" instead of using site wide assembly using modification date or something on IIS7 ?
2) Also if I have some sort of "static" asset like - http://static.domain.com/js/123.js - how can I use rewrite to send the latest file for a request if someone has integrated this static link onto their site ?
i.e. http://static.domain.com/js/123.js is the link and when a request comes for this - check and send latest file ?

ASP.NET 4.5+ comes with a built-in bundling & minification framework
which is designed to solve this problem.
If you absolutely need a simple roll-your-own solution you can use the answer below, but I would always say the correct way is to use a bundling & minification framework.
You can modify 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.
Create an UrlHelperExtension class that will help get this information when needed in the 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" />

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)

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.

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)

This question is really old now, but if anyone stumbles upon it, here's to my knowledge the current state of the art:
In ASP.NET Core you can use TagHelpers and simply add the asp-append-version attribute to any <link> or <script> tag:
<script src="~/js/my.js" asp-append-version="true"></script>
For both ASP.NET Core and Framework there is a NuGet Package called WebOptimizer (https://github.com/ligershark/WebOptimizer). It allows for both bundling and minification, and will also append a content-based version string to your file.
If you want to do it yourself, there is the handy IFileVersionProvider interface, which you can get from your IServiceProvider in .NET Core:
// this example assumes, you at least have a HttpContext
var fileVersionProvider = httpContext.RequestServices.GetRequiredService<IFileVersionProvider>();
string path = httpContext.Content("/css/site.css");
string pathWithVersionString = fileVersionProvider.AddFileVersionToPath(httpContext.Request.PathBase, path);
For .NET Framework, you can get the FileVersionProvider source from here: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Razor/src/Infrastructure/DefaultFileVersionProvider.cs
You will have to do some work, like replacing the Cache with MemoryCache.Default or a ConcurrentDictionary or something, but the 'meat' is there.

Related

Custom script transform in ASP.NET MVC bundle ignored when debug is false and minified file exists

I'm working on an MVC 5.0 (.Net 4.5) application where I need to apply a custom JavaScript transform to my included bundle files. One of those files, which I'm calling dummy.js for illustration purposes, has a minified file called dummy.min.js.
I created a custom script transform to replace injected window.jQuery references with a different expression. Everything works fine when I run locally and in debug mode, but when debug mode is turned off in the Web.config file, the Bundle returns the contents of the dummy.min.js file, but my script transform is not applied to it. It only gets applied to JavaScript files that don't have an associated .min.js file.
Does anyone have an idea on how to resolve this? It almost sounds like a bug in MVC.
A workaround is to remove the minified file. This post kind of addresses my situation by suggesting removing the .min.js file since MVC minifies by default, but I'm looking for an alternative solution (if any).
Thank you so much in advance.
Here's how to reproduce the above:
If you're interested in reproducing my issue, here's a quick BundleConfig and the actual custom script transform. It replaces all instances of window.jQuery with window.$jq1_9||window.jQuery, assuming it is injected via a self-executing anonymous function.
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(
new ScriptBundle("~/bundles/dummy")
.Include("~/Scripts/dummy.js", new InjectedJQueryVariableRewriteTransform()));
}
}
public class InjectedJQueryVariableRewriteTransform : System.Web.Optimization.IItemTransform
{
public string Process(string includedVirtualPath, string javaScriptCode)
{
// TODO: I understand this approach is naiive, but it does the trick for now.
return javaScriptCode.Replace("window.jQuery", "window.$jq1_9 || window.jQuery");
}
}
If you have Visual Studio 2012 and MVC 4, you will need version 1.1.0 of the System.Web.Optimization assembly, which you can obtain by running the following command in the Nuget Package Manager. At time of writing it installs version 1.1.2 of the package.
Install-Package Microsoft.AspNet.Web.Optimization
Here's the sample JavaScript dummy.js. You can create a copy of it and name it dummy.min.js:
(function ($) {
"use strict";
// TODO: Do something interesting...
})(window.jQuery);
Set the debug attribute to false in the following element in Web.config:
<compilation debug="false" targetFramework="4.5" />
Assuming the application's port is 9221, render the bundle in Firefox or Chrome:
http://localhost:9221/bundles/dummy
You will see that when debug is set to true, the transform is applied, as shown below:
(function(){"use strict"})(window.$jq1_9||window.jQuery)
When it is set to false. It is ignored and only the .min.js file is used:
(function(){"use strict"})(window.jQuery)
If you add this line:
bundles.FileExtensionReplacementList.Clear();
you will remove the rule for using .min files when bundling is enabled. You will remove all rules, unfortunately, so if you need any of the other ones you'll need to add them manually. Also, this will change the rules for all bundles.
If you just want to disable these replacement rules for just one bundle, you can just set the EnableFileExtensionReplacements property to false on that specific bundle:
var bundle = new ScriptBundle("...");
bundle.EnableFileExtensionReplacements = false;

ASP.NET 4.5 Bundling in Debug Mode - Stale Resources

Is there any way we can make the ASP.NET 4.5 Bundling functionality generate GUID's as part of the querystring when running in debug mode (e.g bundling turned OFF).
The problem is when developing locally, the scripts/CSS files are generated like this:
<script type="text/javascript" src="/Content/Scripts/myscript.js" />
So if i change that file, i need to do a hard-refresh (sometimes a few times) to get the file to be picked up by the browser - annoying.
Is there any way we can make it render out like this:
<script type="text/javascript" src="/Content/Scripts/myscript.js?v=x" />
Where x is a GUID (e.g always unique).
Ideas?
I'm on ASP.NET MVC 4.
Until the NuGet package is patched as per the other answer above, for now i've ended up using the same wrapper code i did for the beta NuGet package:
private static IHtmlString JsUnbundled(this HtmlHelper htmlHelper, string bundlePath)
{
var jsBuilder = new StringBuilder();
foreach (var file in BundleResolver.Current.GetBundleContents(bundlePath))
{
var tagBuilder = new TagBuilder("script");
tagBuilder.Attributes["src"] = file.AddCacheKey(); // add GUID
tagBuilder.Attributes["type"] = "text/javascript";
jsBuilder.AppendLine(tagBuilder.ToString());
}
return MvcHtmlString.Create(jsBuilder.ToString());
}
I then have another HTML helper which checks if debug, then uses the above - otherwises uses Scripts.Render.
Obviously this doesn't do any kind of hashing of the file - it will ALWAYS request the file. But i don't mind this, as it's only for debug.
We don't currently examine the contents of the files in debug mode, but we could add this feature.
I filed it as an issue on codeplex here.
Try HashCache: https://github.com/kemmis/System.Web.Optimization.HashCache
Execute the ApplyHashCache() extension method on the BundlesCollection Instance after all bundles have been added to the collection.
BundleTable.Bundles.ApplyHashCache();
This will add content hashes to the script/style tags output when in debug mode.

Minified script only in MVC4 BundleConfig

I am adding the following ScriptBundle in BundleConfig:
bundles.Add(new ScriptBundle("~/bundles/javascript").Include(
"~/Scripts/jquery-1.*",
"~/Scripts/load-image.min.js",
"~/Scripts/bootstrap.*",
"~/Scripts/bootstrap-image-gallery.*",
"~/Scripts/my.global.js"));
This is referenced at the end of my _Layout.cshtml as:
#Scripts.Render("~/bundles/javascript")
When debugging I notice that the output of this script rendering is:
<script src="/Scripts/jquery-1.8.2.js"></script>
<script src="/Scripts/bootstrap.js"></script>
<script src="/Scripts/bootstrap-image-gallery.js"></script>
<script src="/Scripts/my.global.js"></script>
Notice the load-image.min.js script is missing? What I want is to use that same minified script whether I'm debugging or not. Under release conditions the script is included in the bundled JS file.
I assume it's seeing the 'min', looking for an un-minified version, not finding one, then deciding what's best is to ignore it entirely. Brilliant. If I make a copy of load-image.min.js, call it load-image.js and then reference it in BundleConfig as "load-image.*" I find it is included in both configurations but what's the point of having to do that?
I assume I'm missing something here. I don't have the un-minified version and I frankly don't care about it. It's used by my Bootstrap image gallery plugin and nothing else. Any ideas out there?
This behavior has been improved (fixed) in the 1.1.0-alpha1 release. We moved all of the old default ignore list entries into a new DirectoryFilter ignore list that are only used when including search patterns like *.js which was the origional intent for this functionality. As a result this should no longer be an issue when you are including individual files explicitly.
Note: the one place this might still be an issue is if you try to include something like jquery-{version}.min.js.
There is ignoreList, which you can clear if you need, it looks like:
public static void AddDefaultIgnorePatterns(IgnoreList ignoreList)
{
if (ignoreList != null)
{
ignoreList.Ignore("*.intellisense.js");
ignoreList.Ignore("*-vsdoc.js");
ignoreList.Ignore("*.debug.js", OptimizationMode.WhenEnabled);
ignoreList.Ignore("*.min.js", OptimizationMode.WhenDisabled);
ignoreList.Ignore("*.min.css", OptimizationMode.WhenDisabled);
return;
}
else
{
throw new ArgumentNullException("ignoreList");
}
}
More details: Advanced Options of ASP.NET Bundling and Minification

Porting static html/javascript site to iPad using trigger.io

Im currently in the process of porting a completely static site using trigger io to convert it to an app. The site comprises of lots of folders in folders with index.html files in them to make the urls nice. The site uses absolute urls to include stylesheets, javascripts, on a tags, and images in every page.
I would like to set a root directory for trigger.io, but I cannot find any way of doing this. Is this even possible?
Cheers,
Rich
Edit:
Example:
<script src="/json.js" type="text/javascript"></script>
<img alt="Bar_hat" class="bar_hat" src="/assets/bar_hat-09efbabebef04dd368425a6b71badfa7.jpg" />
The script tag is in all of the files.
The img tag is used in 90% of the files. These are obviously not being found from within the app.
Copy your "assests" directory to the "src" directory and use without a "slash" before assets -
<img alt="Bar_hat" class="bar_hat" src="assets/bar_hat-09efbabebef04dd368425a6b71badfa7.jpg" />
Also, if you want to access via javascript you must use this pattern:
forge.file.getUrl("assets/bar_hat-09efbabebef04dd368425a6b71badfa7.jpg",
function(file) {
// If using zepto or jquery
$("#whateverImage").attr("src", file);
},
function(err) {
// error
}
);
Edit: getUrl vs getLocal

ScriptManager in an MVC project trying to load MicrosoftAjax js files from a strange path

I have a WebForms app that I'm converting to MVC, but for now running legacy stuff side-by-side.
For some reason, the ScriptManager left to it's own devices tries to load the following files from a very strange (and non-existent) location:
<script src="Scripts/WebForms/MsAjax/MicrosoftAjax.js" type="text/javascript"></script>
...
<script src="Scripts/WebForms/MsAjax/MicrosoftAjaxWebForms.js" type="text/javascript"></script>
I can't find the setting of that location, and googling "Scripts/WebForms/MsAjax" brings back nothing.
Changing the (obsolete) ScriptPath property on the ScriptManager does nothing to help with these two scripts.
Trying to override the Path location like the following also does not work (it just tries to load both scripts)
Scripts.Add(new ScriptReference { Name = "MicrosoftAjax.js", Path = ContextUtil.MapApplicationPath("~/My/Script/Location/MicrosoftAjax.4.0.js") });
Scripts.Add(new ScriptReference { Name = "MicrosoftAjaxWebForms.js", Path = ContextUtil.MapApplicationPath("~/Shared/Scripts/Legacy/MicrosoftAjax/MicrosoftAjaxWebForms.4.0.js") });
What I can't understand is
Why is it not loading the scripts by default from a Embedded Resource?
Where is this strange path coming from?
Why won't it accept my overridden script paths?
Can anyone help?

Resources