Ok, I just discovered a nasty little problem with ScriptReferences to script files that are embedded in assemblies that are installed in the GAC… starting with System.Web.Extensions itself.
First, in case you’re not already familiar with this subject, the way scripts are referenced when they are embedded into assemblies is by building a URL to the ScriptResource.axd handler that is provided by the ASP.NET AJAX server side runtime. The URL that is build includes two query string parameters:
- “d” – this is a encoded/hashed copy of the assembly identity that includes it’s typical .NET assembly identity info (name, version info, etc.)
- “t” – this is a timestamp parameter that is taken from the assembly’s last modified date in terms of the file system.
Second it’s important to realize that when assemblies are placed into the GAC they actually receive a new last modified time on the copy of the assembly that is put into %SYSTEMROOT%Assembly. This is news to me personally, but then again it never mattered before because the times on those files never mattered in the traditional .NET application. So, if you’re installing a GAC based assembly on multiple servers in a web farm, there’s virtually no way that the last modified time on the assembly on one server will end up matching the time on the other servers… unless you were using a virtual server image of course.
So, with those two details out there, I’m sure you already see the problem: the “t” parameter will be emitted differently from each server in your farm resulting in different URLs.
So why is this a problem? Well your end users hit www.foobar.com and ServerA fields the first request. ServerA’s ScriptReference to Microsoft.Ajax.js serves up a ScriptResource.axd URL with a
t=1234 (just an example). Now the user does something that requires a new page to be served up and that request ends up going to ServerB. Well ServerB’s ScriptReference to Microsoft.Ajax.js ends up serving up a ScriptResource.axd URL of
t=5678 and guess what? Yup, the user has to download that rather large script file all over again because as far was their browser knows the URL is different so the content must be different.
That’s the typical problem most people using ASP.NET AJAX will have. Non-runtime, silent, not deadly, but killing performance none-the-less. Another problem you can have if you’re doing more advanced work is if you dynamically load scripts (using the ScriptLoader) the URLs will not match and you’ll potentially end up trying to load the same script twice and you’ll end up with a runtime error.
This may not seem all that significant, but consider that it will happen for every script that is served out of a GAC based assembly you reference in your web project. Depending on how large those files are and how many servers you have in your farm you may be causing your end users quite a bit of network load/startup time.
IMHO, the inclusion of the “t” query string parameter flies in the face of the power of .NET assembly identity. They’ve taken a well defined, working system which was already being leveraged for the “d” parameter and completely broke it by including another piece of information to identify the assembly. The only plausible explanation for the “t” parameter is that they want to support the “lesser” developers out there who aren’t incrementing their assembly version numbers correctly or maybe has to do with the “pure” web project style projects (which I never use personally, always use web application projects), but I’d be willing to bet they don’t even have any version information by default.
So how can you solve this problem? Well there’s several ways that I can think of, none of them great:
- You can move to host your GAC’d scripts using the file system, but this completely circumvents the entire benefit of having them in the assembly in the first place.
- Handle the ScriptManager.ResolveScriptReference event and write the script paths yourself without the “t” parameter. The problem with this approach is that if you wanted to build links to ScriptResource.axd you’re up the creek without a paddle because all of the methods associated with building these URLs are marked as “internal” within the System.Web.Extensions assembly.
- You can choose to “Copy Local” the GAC based assemblies in your build, but this really defeats the purpose of assemblies being installed in the GAC in the first place and has farther reaching implications for your applications.
- You can manually go and set the last modified time on those assembly files in the GAC across all your servers using a script of some kind.
We’ve opted for #4 here at Mimeo. We’re using a PowerShell script to set the FileInfo::LastWriteTime on the GAC based assemblies to a fixed date for all the servers. It’s a hack, sure, but at least it’s a server configuration hack and not a runtime hack.
Finally, I have submitted a bug to the connect.microsoft.com site about this problem. Please go and vote on it if you feel it’s as important as I do.