Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Caching WCF javascript proxy on browser

5.00/5 (4 votes)
4 Apr 2012CPOL3 min read 41.3K   236  
WCF Javascript Proxies (Service.svc/js) are never cached. They get generated and downloaded on every page view thus increasing page download time and server CPU. Here's an HttpModule to cache WCF Javascript Proxy on browser and respond with HTTP 304, if unchanged.

Download WcfJavascriptProxyCaching.zip 

Introduction 

When you use WCF services from Javascript, you have to generate the Javascript proxies by hitting the Service.svc/js. If you have five WCF services, then it means five javascripts to download. As browsers download javascripts synchronously, one after another, it adds latency to page load and slows down page rendering performance. Moreover, the same WCF service proxy is downloaded from every page, because the generated javascript file is not cached on browser. Here is a solution that will ensure the generated Javascript proxies are cached on browser and when there is a hit on the service, it will respond with HTTP 304 if the Service.svc file has not changed.  

Problem 

Here’s a Fiddler trace of a page that uses two WCF services.

Image 1

You can see there are two /js hits and they are sequential. Every visit to the same page, even with the same browser session results in making those two hits to /js. Second time when the same page is browsed:

Image 2 

You can see everything else is cached, except the WCF javascript proxies. They are never cached because the WCF javascript proxy generator does not produce the necessary caching headers to cache the files on browser. 

Solution 

Here’s an HttpModule for IIS and IIS Express which will intercept calls to WCF service proxy. It first checks if the service is changed since the cached version on the browser. If it has not changed then it will return HTTP 304 and not go through the service proxy generation process. Thus it saves some CPU on server. But if the request is for the first time and there’s no cached copy on browser, it will deliver the proxy and also emit the proper cache headers to cache the response on browser.

In order to get a service cached, first you will have to change the way you put a reference to the service.

ASP.NET
<asp:ScriptManager runat="server">
  <Services>
    <asp:ServiceReference Path="~/(v2)/Service1.svc" />
  </Services>
</asp:ScriptManager>

Notice the (v2) on the service reference. That’s the version number. Whenever you make some changes to the service and you want browsers to download latest copy, you change the version. Unless this version is there on the URL, the service proxy isn’t cached. This is a safeguard mechanism. It prevents javascript proxies getting cached on browser and you having no way to update them unless you change the service names.

The HttpModule first intercepts the BeginRequest to see if the requested URL is a WCF Service proxy and whether it is versioned. If it is, then it checks if the copy cached on browser is out dated. If the .svc file has been changed in the meantime, then the cached copy is outdated. If not, then it returns a HTTP 304 and ends the request.

C#
public sealed class JavascriptProxyCacheModule : IHttpModule
{
  private HttpApplication app;
      
  public void Init(HttpApplication context)
  {
    app = context;
    app.EndRequest += new EventHandler(app_EndRequest);
    app.BeginRequest += new EventHandler(app_BeginRequest);
  }

  void app_BeginRequest(object sender, EventArgs e)
  {
    var path = app.Context.Request.Path.ToLower().Replace('\\', '/');
    if (IsWcfJavacsriptProxy(path))
    {
      // versioning is used by using (xxx) in the path of the script
      // reference. for ex, /(v2)/Service.svc/js
      // In this case, remove the (v2)/ 
      int pos = path.IndexOf('(');
      if (pos > 0)
      {
        int endPos = path.IndexOf(')');
        path = path.Substring(0, pos) + path.Substring(endPos + 2);
        app.Context.RewritePath(path);

        string ifModifiedSince = app.Context.Request.Headers["If-Modified-Since"];
        if (!string.IsNullOrEmpty(ifModifiedSince))
        {
          string filePath = app.Context.Request.PhysicalPath;
          DateTime fileLastModifyDateTime = File.GetLastWriteTime(filePath);
          DateTime browserLastModifyDateTime;
          if (DateTime.TryParse(ifModifiedSince, out browserLastModifyDateTime))
          {
            if ((browserLastModifyDateTime - fileLastModifyDateTime).Seconds > 5)
            {
              app.Context.Response.StatusCode = 304;
              app.Context.Response.End();
            }
          }
        }
      }        
    }
  }

It also hooks the EndRequest to make sure when a WCF Service proxy is delivered to the browser, the proper caching headers are sent so that browser caches the javascript for a certain duration. Feel free to change the cache expiration policy.

C#
private void app_EndRequest(object sender, EventArgs e)
{
  var path = app.Context.Request.Path.ToLower().Replace('\\', '/');
  
  if (app.Context.Response.StatusCode == 200
    || app.Context.Response.StatusCode == 202)
  {
    if (IsWcfJavacsriptProxy(path) && IsVersioned())
    {
      var lastModified = DateTime.UtcNow;
      // Expire by tomorrow 7 AM always.
      var expires = lastModified.AddDays(1).Subtract(lastModified.TimeOfDay).AddHours(7);
      HttpCachePolicy cache = app.Context.Response.Cache;
      cache.SetLastModified(lastModified);
      cache.SetExpires(expires);
      cache.SetCacheability(HttpCacheability.Public);
    }
  }      

}

In order to use it, you need to add this httpmodule in the web.config:

XML
<httpModules>
  <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
  <add name="JavascriptProxyCacheModule" type="WcfService1.JavascriptProxyCacheModule"/>
</httpModules>

This solution only works in IIS and IIS Express. It does not work in ASP.NET development server because the development server does not allow (v2) on the service URL.  

Image 3

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)