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.
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:
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: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.
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))
{
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.
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;
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:
<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.