Introduction
ASP.NET 1.x provided excellent support for caching user controls using directive @outputcach
and PartialCaching
attribute. Also, it provides some mechanism to remove a cached user control from cache if data has been changed. Specifically, cached user controls are removed from cache if dependent file has changed or dependent cached item has changed. (Check MSDN and ASP.NET Caching for details).
In reality, the content of a user control depends on data mostly from database, rarely from files, and never from cache. It is therefore fair to say that cache dependency provided by ASP.NET 1.x is not useful out of box for user control cache removal. Developers have to come up with ways to extend and enhance this cache dependency just like what were done in Dino Esposito's article using timer pulling database, and Jeff Prosese's article using database extended stored procedure interacting with file system.
This article takes a little bit different approach: instead of monitoring changes in database table using timer or file system change notification, we will monitor the internal state of the user control that uniquely determines the data to be loaded. For example, suppose we login to a broker account and do some transaction, then the content of the transaction history user control will be determined by Session ID and Account number, for the following reasons:
- the cached transaction history user control must vary by Session ID, not allowing private data be disclosed to different login users.
- the cached transaction history user control will be removed from cache if transaction processing user controls with matching account number is loaded for use.
In essence, we have to correlate Transaction History User Control to Transaction Processing User Control. And consequently, when a user is trying to do a transaction for an account, we will remove the cached History
user control immediately. Now, let me explain how to use "VaryByCustom
", CacheDependency
class, and custom attribute to implement this cache removal strategy.
Multi version Caching of User Control
There is only one way to access a cached user control correctly assuming we drag and drop History
onto IDE:
History h = (History) FindControl("History1");
if( h !=null) h.AccountNumber=this.tbAccountNumberRetrival.Text;
You may argue that we can use the following code to load cached user control and change its property AccountNumber
:
PartialCachingControl pcc= (PartialCachingControl) LoadControl("History.ascx");
this.TheLocation.Controls.Add(pcc);
if (pcc.CachedControl !=null)
((History) pcc.CachedControl).AccountNumber=this.tbAccountNumberRetrival.Text;
But as shown in my sample project included with this article, LoadControl
approach will not work for cached user control (please reference commented-out code snippet in Page_Load
function in WebForm1.aspx.cs). Therefore, we will stay with FindControl
function and declarative loading (drag and drop) of cached user control, and programmatically set its state for content changes.
Unfortunately, the above approach is not sufficient enough for multi-version caching since after the user control is cached, FindControl
will return null
and therefore we cannot reference its property AccountNumber
. As it turns out, ASP.NET disallows programmatically changing caching content, except using VaryByCustom
or VaryByControl
. In other words, when the custom string or control value changes, FindControl
function will return not-null reference to the user control and therefore allow us to set its property to retrieve multi-version content. Here are the related code segments utilizing VaryByCustom
from ASP.NET:
<%@ OutputCache Duration="60" VaryByParam="None"
VaryByCustom="CachingMultiVersion" %>
In Global.asax:
public override string GetVaryByCustomString(HttpContext context, string custom)
{
if ( custom.ToLower() =="cachingmultiversion")
{
HttpCookie cookie = context.Request.Cookies["CachingMultiVersion"];
if(cookie != null) return cookie.Value;
}
return base.GetVaryByCustomString (context, custom);
}
public static string CachingMultiVersion
{
set
{
HttpContext.Current.Response.Cookies["CachingMultiVersion"].Value=value;
}
}
In WebForm1.aspx:
Global.CachingMultiVersion=this.tbSessionID.Text +
this.tbAccountNumber.Text;
In essence, we need to correctly vary CachingMultiVersion
custom string so ASP.NET will give us a chance to use FindControl
function. This seems to be the only way to implement multi-version User Control caching correctly.
Removal of Cached Content of a user control:
Now we know how to do multi-version caching of user control per "Account Number". Let us look at how to remove cached user control History.ascx when another user control Processing.ascx is loaded with matching "Account Number". The answer lies in CacheDependency
class. In fact, ASP.NET generates StaticPartialCachingControl
or PartialCachingControl
, both of which are derived from BasePartialCachingControl
. Therefore, we can use BasePartialCachingControl
's Dependency
property to establish a dependency with a cached item:
pcc= Parent as PartialCachingControl;
spcc= Parent as StaticPartialCachingControl;
if (pcc !=null) pcc.Dependency=new CacheDependency(null,
new string[]{CachingKey});
if (spcc !=null) spcc.Dependency=new CacheDependency(null,
new string[]{CachingKey});
The code is actually in Base.ascx.cs, from which all user controls will be derived. Therefore, all cached user controls can be dependent on some cached item. Consequently, Process.ascx just needs the following code to remove the related cached item in order to remove the cached user control History.ascx:
HttpRuntime.Cache.Remove(CachingKey);
Most important, we need to build CachingKey
by relating Monitored Parameter "AccountNumber
" on History.ascx and Processing.ascx so that only matched caching will be removed. Here is the code for applying attribute CorrelatedCaching
in order to build CachingKey
correctly:
Code in History.ascx.cs:
[PartialCaching(60)]
[CorrelatedCaching(CorrelatedCachingActionOption.Caching,"CorrCachingName1")]
public class History : JQD.BaseUserControl
{
......
string _AccountNumber;
[CorrelatedCaching(CorrelatedCachingActionOption.Monitoring,
"AccountNumber")]
protected System.Web.UI.WebControls.TextBox tbAccountNumber;
public string AccountNumber;
{
get { return _AccountNumber;}
set {
this.tbAccountNumber.Text=value;
_AccountNumber=value;
}
}
Code in Processing.ascx:
[CorrelatedCaching(CorrelatedCachingActionOption.Monitoring,"AccountNumber")]
protected string _Prop1;
public string Prop1
{
get { return _Prop1;}
set{ _Prop1=value;}
}
It is very important to understand that applying attribute is like passing initialization data to a class. That class must implement a special function to take data from attributes. I implemented a function RegisterForCorrelatedCaching
to just do that and here are some of the code segments:
object[] obj =
this.GetType().GetCustomAttributes(typeof(CorrelatedCachingAttribute), true);
CorrelatedCachingAttribute corrCachingAttr = (CorrelatedCachingAttribute) obj[0];
MonitoringString="";
BuildMonitoringString();
CachingKey=corrCachingAttr.Name + MonitoringString;
private void BuildMonitoringString()
{
Type t = this.GetType();
FieldInfo[] allFields= t.GetFields( ...);
foreach (FieldInfo fi in allFields)
{
CorrelatedCachingAttribute[] attr =
(CorrelatedCachingAttribute[])
fi.GetCustomAttributes(typeof(CorrelatedCachingAttribute),true);
if (attr[0].Action ==CorrelatedCachingActionOption.Monitoring)
MonitoringString += attr[0].Name +"="+fi.GetValue(this).ToString()+";";
}
}
What we are doing here is to loop through all User Control properties and pick those with CorreletedCaching
attribute applied and with CorrelatedCachingActionOption.Monitoring
. Then we will get those properties' values using reflection, and build MonitorString
. CachingKey
will be the concatenation of MonitorString
and Correlation Name, with the Correlation Name being present in the attribute applied to the class History
and the class Processing
. This makes correlation of user controls possible by virtually having the same CachingKey
.
How to run the sample project
- Download the zip file and extract it into C:\inetpub\wwwroot.
- Click on the solution file to open it up in VS.NET 2003, and build Solution. Note that we may need to use IIS MMC to configure directory C:\inetpub\wwwroot\TestCorrelatedCaching as application to enable running in debug mode.
- Run in Debug mode and you should see the following screen:
- Set SessionID and AccountNumber, and click on "CachingMultiVersion", and then "Submit from page". You should see
History
User Control timestamp changes once, and then stay the same.
- Now change AccountNumber and set AccountNumber for content retrieval, and click on "CachingMultiVersion" and then "Submit from page". You should see timestamp changes once and AccountNumber show up in
History
User Control and then stay the same.
- Finally, input matching AccountNumber into Account Number for caching removal, and then click "Submit from page". You will see the timestamp constantly changing, indicating
History
User Control is not cached anymore. But if you input a mismatched AccountNumber, the timestamp will not change.
Conclusion
Correlated Caching is actually a very simple idea if you are comfortable using attributes and reflection. I intended to use it in my current project and hope you will also find it useful.