Prevent .js caching in asp.net

This is like a very common issue, specially for those who are working on public site which is live and they have to release the builds every week or month and if the new build contain JS files then your change will not reflect on the client browser until someone there presses ctrl + F5.

So, after googling this issue. I came to know it is possible to prevent the browser from accessing the cache copy by writing the script tag as below

   1: <script type="text/javascript" src="../Includes/main.js?random=556"></script>

 

It is good, when I have a single or some number of pages to change. Unfortunately, That is not the case I have hundreds of pages and that will be hassle to make those changes again and again for every build. So, I try to make this thing happen using Response Filter.

Create Response Filter:

So, to write the Response Filter we need to create a class and which is extended from Stream (System.IO.Stream) and I named it BuildTokenFilter.

   1: Imports Microsoft.VisualBasic
   2: Imports System.IO
   3: Imports System.Text.RegularExpressions
   4: Imports System.Configuration
   5:  
   6:  
   7: Public Class BuildTokenFilter
   8:     Inherits Stream
   9:     Private _responseStream As Stream
  10:     Public Sub New(ByVal responseStream As Stream)
  11:         _responseStream = responseStream
  12:     End Sub
  13:     Public Overrides ReadOnly Property CanRead() As Boolean
  14:         Get
  15:             Return _responseStream.CanRead
  16:         End Get
  17:     End Property
  18:     Public Overrides ReadOnly Property CanSeek() As Boolean
  19:         Get
  20:             Return _responseStream.CanSeek
  21:         End Get
  22:     End Property
  23:  
  24:     Public Overrides ReadOnly Property CanWrite() As Boolean
  25:         Get
  26:             Return _responseStream.CanWrite
  27:         End Get
  28:     End Property
  29:  
  30:     Public Overrides Sub Flush()
  31:         _responseStream.Flush()
  32:     End Sub
  33:  
  34:     Public Overrides ReadOnly Property Length() As Long
  35:         Get
  36:             Return _responseStream.Length
  37:         End Get
  38:     End Property
  39:  
  40:     Public Overrides Property Position() As Long
  41:         Get
  42:             Return _responseStream.Position
  43:         End Get
  44:         Set(ByVal value As Long)
  45:             _responseStream.Position = value
  46:         End Set
  47:     End Property
  48:  
  49:     Public Overrides Function Read(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer) As Integer
  50:         Return _responseStream.Read(buffer, offset, count)
  51:     End Function
  52:     Public Overrides Function Seek(ByVal offset As Long, ByVal origin As System.IO.SeekOrigin) As Long
  53:         _responseStream.Seek(offset, origin)
  54:     End Function
  55:  
  56:     Public Overrides Sub SetLength(ByVal value As Long)
  57:         _responseStream.SetLength(value)
  58:     End Sub
  59:     Public Overrides Sub Write(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer)
  60:         Dim strRegex As String = "src=(?<Link>.*js)"
  61:         Dim BuildTokenString As String = "?token=" & IIf(ConfigurationManager.AppSettings("BuildToken") = Nothing, "1.0", ConfigurationManager.AppSettings("BuildToken"))
  62:         Dim objRegex As New Regex(strRegex)
  63:         Dim html As String = System.Text.Encoding.UTF8.GetString(buffer)
  64:         Dim extCharCount As Integer = 0
  65:  
  66:         Dim objCol As MatchCollection = objRegex.Matches(html)
  67:         For Each m As Match In objCol
  68:             extCharCount += BuildTokenString.Length
  69:             Dim newJSValue As String = m.Value & BuildTokenString
  70:             html = html.Replace(m.Value, newJSValue)
  71:         Next
  72:         buffer = System.Text.Encoding.UTF8.GetBytes(html)
  73:         _responseStream.Write(buffer, offset, count + extCharCount)
  74:     End Sub
  75: End Class

"Write" is the function which is doing the whole stuff. It is really simple to understand. Let me go linewise.

60. Create a string variable and assign and regular expression which will search all the js files tags in html
61. Create a string which will append to the JS file. Checking the App key in web.config So that the token can be extended in future.
62. Create a Regex Object
63. Get the html from Buffer
64. Create an integer variable which will keep track of the characters that are added to the html variable. 
66. Save the matches in a collection
67. Get each mach object from Match Collection
68. Add the character count which will added to the html variable
69. Create a variable and assign value of match and concatenated with build token
70. Replace the old value with the new with build token in html variable
72. Encode the html variable back to the buffer
73. Write the buffer to the stream by adding count with build token character count.

Now lets create a HttpModule which will responsible of attaching this response filter with every request.

Create HttpModule:

Now to write HttpModule, I will create a class which will Implements IHttpModule (interface) and I name it "BuildToken"/

   1: Imports Microsoft.VisualBasic
   2: Imports System.Web
   3: Public Class BuildToken
   4:     Implements IHttpModule
   5:     Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
   6:  
   7:     End Sub
   8:     Public Sub Init(ByVal context As System.Web.HttpApplication) Implements System.Web.IHttpModule.Init
   9:         AddHandler context.BeginRequest, AddressOf Application_BeginRequest
  10:     End Sub
  11:     Private Sub Application_BeginRequest(ByVal source As Object, ByVal e As EventArgs)
  12:         Dim context As HttpApplication = CType(source, HttpApplication)
  13:         If context.Request.RawUrl.Contains(".aspx") = True Then
  14:             context.Response.Filter = New BuildTokenFilter(context.Response.Filter)
  15:         End If
  16:     End Sub
  17: End Class

 

In this case the magical function is Application_BeginRequest which can easily be understand but the question might arises why I have put the If condition on line number 13. Well, I don't want my module to attach response filter against all the files. Keep in mind, HttpModule is always called no matter what content type are you requesting. when somebody write http://www.sitename.net/images/border2.jpg it will still process through HttpModule and that is what I don't want.

Configuration Setting:

We are almost done, just a web.config entry is left which we keep for the modification of token string.

   1: <appSettings>       
   2:     <add key="BuildToken" value="7.0"/>
   3: </appSettings>

 

Test the filter:

Now to check the the response filter create a page that is called default.aspx and paste the following markup

   1: <html xmlns="http://www.w3.org/1999/xhtml" >
   2: <head runat="server">
   3:     <title>Untitled Page</title>
   4:     <script type="text/javascript" src="../Script/samain.js"></script>
   1:  
   2:     <script type="text/javascript" src="../ControlsScripts/preloadshare.js">
   1: </script>
   2:     <script type="text/javascript" src="../Script/mainview.js">
</script>
   5: </head>
   6: <body>
   7:     <form id="form1" runat="server">
   8:     <div>
   9:     <p>
  10:     Right click and view source you will find JS files with query string</p>
  11:     </div>
  12:     </form>
  13: </body>
  14: </html>


Now Run the application and view source the page you will notice some thing like give below.
sc_responsefiltercache

Each time, when you made some changes in JS file just change Build Token entry in the web.config file and your visitors will get the updated copy of JS. You can also download the project files given below. 

 

6 Comments

  • What happens if you haven't changed all of the javascript files, and have just made a change to one of them? The client will be forced to request files that it could have just used from the cache making the page load slower.

  • This is one reason why many libraries ship their .js files with specific version numbers for each release which seems annoying at first, but makes sense in hindsight.

    The approach you have fixes the problem but puts the loading of static script files off on ASP.NET code vs. letting IIS handle it natively which is faster, provides for effective caching and compression etc. Definite tradeoff.

  • Maybe it's just me, but if you can create a module dat uses a regex to replace some values, you can use a console app to replace that value in your sourcefiles just as well. This in turn would be a lot more efficient.

    Instead of creating a module that does the heavy work of regex matching and string replacement you could also create a WebControl and instead of a tag add this webcontrol in your page head. The webcontrol in turn can get a build number from the assembly and automagically append that built number to your querystring.

    You should also consider a redisign of your application if you have a lot of code duplication. Maybe you could use a masterpage where you place the tag just once.

    Response Filters are pretty heavy and quite errorprone so I'm not really fond of them... as you probably know by now :D

  • Seems like a lot of extra processing. My method is to store an application setting in web.config and to simply output that variable on the page. Since its from web.config you can easily update the value when you update the scripts.

    I usually create a static readonly string who's value is that of the application setting key found in the web.config file. Then I can access the value from anywhere in the application like so:

    myNamespace.settings.settingName

    then I just have a normal script tag that writes out the variable above as part of the script src value.

    This is for performance reasons - we do not want to process the html with regex on every single request. This is sure to cause performance hiccups. Instead, we store the value in a readonly static string that we can access from anywhere in the application without needing to instantiate any additional objects.

  • @Mark
    What about if you skip the token logic, and place the JS file last modified date in query string for each JS. In this way the browser will download only the file which is modified.

    @webbes
    Totally agree with you, Masterpages are the very easy and effective way of doing this but changing the source code from console application will not that feasible, I mean altering the source code from application does not make sense to me, what if your project is under source control like TFS are you going to check out all the source code ?

    @Bryan Migliorisi
    I thought I have learned many ways from you guys. But the matter of fact is I had to implement the way I describe because I have 400+ pages and hundreds of JS file with hundreds of controls without master pages and the best part is I had to release the build just with in 2 days. So that's the quickest method as I see.

    INSHALLAH, I will write detailed stuff on this subject by implementing your suggestions :)

    Anyway, Thanks alot for the feedback

  • Sometimes you just have to come up with a pragmatic solution like you did.

    Cheers,
    Wes

Comments have been disabled for this content.