Background
Cache is one of the most important aspects of building high performance and scalable web applications. As one of the key features in ASP.NET AJAX, calling a script method from a client would be used a lot when building AJAX applications using ASP.NET. The following is a sample to show how to enable server-side caching.
<asp:ScriptManager ID="ScriptManager1" runat="server" ScriptMode="Debug">
<Services>
<asp:ServiceReference Path="CacheService.asmx" />
</Services>
</asp:ScriptManager>
<script language="javascript" type="text/javascript">
var count = 0;
function getServerTime()
{
window.count ++;
CacheService.GetServerTime(onSucceeded);
}
function onSucceeded(result)
{
Sys.Debug.trace(result.format("HH:mm:ss"));
if (count < 6)
{
window.setTimeout(getServerTime, 3000);
}
elses
{
window.count = 0;
}
}
</script>
<input type="button" value="GetCurrentTime" onclick="getServerTime()" />
<br /><br />
<textarea cols="20" rows="10" id="TraceConsole"></textarea>
There's a ScriptManager
control with the ScirptMode
property set to Debug
so that we could use the Sys.Debug.trace
method to display messages in the TextArea
element whose ID is "TraceConsole
". We'll access the method on the server to get the server time for six times when we click the button, and there's a 3-seconds interval between each two successive ones. The script method is defined on the server-side like this:
[ScriptService]
public class CacheService : System.Web.Services.WebService
{
[WebMethod]
public DateTime GetServerTime()
{
return DateTime.Now;
}
}
Open the page and click the button, and we'll see the following result on the page:
Cache on server-side
Script method access in ASP.NET AJAX has a built-in cache mechanism on the server-side but it seems that only a few developers realize that. They always cache the data in the HttpContext.Cache
object or some other places and get the data from the cache, if necessary. That's one of the common ways of caching when developing ASP.NET applications, but we can use a more convenient and efficient feature some times. Please look at the following code to see how to enable this feature:
[WebMethod(CacheDuration=10)]
public DateTime GetServerTime()
{
return DateTime.Now;
}
Like when building web services in ASP.NET, we use the same way to let ASP.NET cache the request for the same resource with the same content, by setting the CacheDuration
property of WebMethodAttribute
. As in the above code snippet, the result of the method would be cached for 10 seconds. The static InitializeCachePolicy
method of the System.Web.Script.Services.RestHandler
class in System.Web.Extensions.dll shows what happens if we do the following.
private static void InitializeCachePolicy(WebServiceMethodData methodData,
HttpContext context)
{
int cacheDuration = methodData.CacheDuration;
if (cacheDuration > 0)
{
context.Response.Cache.SetCacheability(HttpCacheability.Server);
context.Response.Cache.SetExpires(DateTime.Now.AddSeconds(
(double) cacheDuration));
context.Response.Cache.SetSlidingExpiration(false);
context.Response.Cache.SetValidUntilExpires(true);
if (methodData.ParameterDatas.Count > 0)
{
context.Response.Cache.VaryByParams["*"] = true;
}
else
{
context.Response.Cache.VaryByParams.IgnoreParams = true;
}
}
else
{
context.Response.Cache.SetNoServerCaching();
context.Response.Cache.SetMaxAge(TimeSpan.Zero);
}
}
If ASP.NET AJAX found that the CacheDuration
is set for the method which is ready to execute, it would pass HttpCacheabibity.Server
to the SetCacheability
method of the current HttpCachePolicy
object so that the result of the request would be cached for future use. If the executing method contains parameters, the "*" item would be set to true
in the HttpCacheVaryByParams
object by the VaryByParams
property of the current HttpCachePolicy
so that ASP.NET would cache separate results for different parameter combinations.
Let's see the effect of caching.
Comparing with caching data programmatically, the most important advantage of setting the CacheDuration
property to enable caching is that it's really easy to use. We can now focus more on the implementations of the methods and get rid of the troubles when working with cache (e.g., synchronization). It also improves the performance a little bit since it's not necessary to serialize the result into a JSON string but ASP.NET would take the responsibility to send the data cached to the client-side. But some times, caching data by ourselves is more appropriate since it would save many resources. For instance, here's a method which accepts four parameters and the second one indicates that the rest should be ignored or not.
public string GetResult(int key, bool ignoreRest, string args1, string args2) { ... }
In the scenario above, almost all programmers would cache the data only for the different values of the key
parameter and will not care what the rest of the parameters are when the second one is set to true
. But ASP.NET cannot realize the meaning of the parameters so it will cache different copies of data for any combination despite the same results.
Cache on client-side
I used the HttpWatch basic edition to capture the communication between the client and the server. Here's the snapshot.
Each time we access the script method, the same content would be posted to the server, receiving the same result. Although the result would be cached, we only save the execution time of the method, and the round trips would be taken each time we invoke the method. It means that if the the size of the result is huge or the bandwidth is low, the accesses to script methods are still time-consuming jobs for the users. So it would be better if we can cache the result on the client-side so that the user can get the result immediately if we call the same method with the same parameters again - even if the network connection is lost.
Here we go.
At first, we can only use the HTTP GET method to access the script method if we want to let the browser cache the result for us.
[WebMethod]
[ScriptMethod(UseHttpGet = true)]
public DateTime GetServerTime() { ... }
We'll use the traditional way to cache the result on the client-side.
[WebMethod]
[ScriptMethod(UseHttpGet = true)]
public DateTime GetServerTime()
{
HttpCachePolicy cache = HttpContext.Current.Response.Cache;
cache.SetCacheability(HttpCacheability.Private);
cache.SetExpires(DateTime.Now.AddSeconds((double)10));
cache.SetMaxAge(new TimeSpan(0, 0, 10));
return DateTime.Now;
}
We set the Cacheabilitiy
to public
(private
is also OK if you want to prevent the response being cached and shared among different clients) and specify a 10-seconds expiration time. We also set the MaxAge
to 10 seconds since ASP.NET AJAX has already set this value to zero (TimeSpan.Zero
). Let's check the effect...never cached? Let's see the header set in one of the responses.
Cache-Control public, max-age=0
Date Fri, 29 Jun 2007 00:44:14 GMT
Expires Fri, 29 Jun 2007 00:44:24 GMT
The problem is the value of max-age
set in Cache-Control
. We have already set it to 10 seconds but it's still zero due to the implementation of the SetMaxAge
method of the HttpCachePolicy
class.
public void SetMaxAge(TimeSpan delta)
{
if (delta < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException("delta");
}
if (s_oneYear < delta)
{
delta = s_oneYear;
}
if (!this._isMaxAgeSet || (delta < this._maxAge))
{
this.Dirtied();
this._maxAge = delta;
this._isMaxAgeSet = true;
}
}
After calling the SetMaxAge
method once, the _isMaxAgeSet
flag is set to true
, preventing _maxAge
from being set to a value smaller than the current one. When executing the script method, _isMaxAgeSet
is true
and _maxAge
is TimeSpan.Zero
so we cannot set it to any other value. It's time to use reflection. What we should do is set the _maxAge
value directly.
[WebMethod]
[ScriptMethod(UseHttpGet = true)]
public DateTime GetServerTime()
{
HttpCachePolicy cache = HttpContext.Current.Response.Cache;
cache.SetCacheability(HttpCacheability.Private);
cache.SetExpires(DateTime.Now.AddSeconds((double)10));
FieldInfo maxAgeField = cache.GetType().GetField(
"_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
maxAgeField.SetValue(cache, new TimeSpan(0, 0, 10));
return DateTime.Now;
}
Let's check the effect.
Seems no different from the previous one, but HttpWatch could tell us this:
Nothing is sent and received, so the cached responses have really been "cached" and the round trips between the client and the server have been saved! Please note that the cache is also "parameter combination specific", which means if you use different parameters, you'll get the new result since the request URL (query string) has been changed.
The cache on the client-side is successful due to the response header the browser received. Please focus on the max-age
value in the Cache-Control
item.
Cache-Control public, max-age=10
Date Fri, 29 Jun 2007 00:54:32 GMT
Expires Fri, 29 Jun 2007 00:54:42 GMT
The most important advantage of caching responses on client-side is that it improves the performance significantly. But it has disadvantages. Perhaps, the most serious one of them is the usage of reflection. Reflection operations are always restricted in some kind of host circumstance. If you want to use reflection, you should follow one of three ways. But actually, there's few chance to apply any one of them in a virtual web hosting.
- Use full trust in your web application.
- Use customized trust level with
ReflectionPermission
in it. - Put the code using reflection in a separate assembly and register it into GAC.
Another disadvantage of caching responses on the client-side is that different clients would execute the server method at least once to get the result before caching. In this aspect, server side caching works better since the server method would be executed only once (for the same parameter combinations) and the cached result could be sent to the requests from all clients. But we could minimize the execution time of the method, e.g., cache the result programmatically on the server-side as we always do.
Conclusion
We have talked about three ways of caching to improvement the performance of script methods access in ASP.NET AJAX, in this article. It's time to draw a conclusion.
- Cache on server side programmatically: It's the most flexible one among the three ways. We could cache anything we want and select any kind of cache policy.
- Cache on server side by setting the
CacheDuration
property: It's the easiest way to cache. The method would be executed only once (for the same parameter combinations) and the cached result could be sent to requests from all clients. Some times, this is not so efficient since ASP.NET would cache more copies of data than we actually need. - Cache on client side: This has the best performance once cached since the round trips between the client and the server are saved. But different clients would execute the server method at least once to get the result before caching.