Implementing Caching in ASP.Net

This article explains the concepts, advantages and types of caching and the implementation of caching in ASP.NET applications. The code also serves as an example of using inline coding, creating user controls, trace mechanism, and so on.

What is Caching?

Caching is the technique of persisting data in memory for immediate access to the requesting program calls. Many in the developer community consider caching as one of the features available to improve performance of web applications.

Why Caching?

Consider a page with a list of Employee names, contact numbers and mail-ids of existing employees of a company on an intranet accessible by all employees. This is very useful information that is available throughout the company and could also be one of the most accessed pages. The functionality of adding, updating or deleting is usually less intensive compared to more transaction-based systems like purchase orders, voucher creation and so on. Now in a normal scenario the process of querying the database for each request is not cost-effective in terms of server resources, hence it is much better to cache or persist the data to avoid this costly loss of resources.

The .NET Advantage

ASP.NET provides the ability in terms of caching at various levels.

1. Page Level Output Caching

This is at the page level and one of the easiest means for caching pages. This requires one to specify the duration of the cache and the attribute of caching.

Syntax: <%@ OutputCache Duration="60" VaryByParam="none" %>

The preceding syntax specifies that the page be cached for 60 seconds and the value "none" for the VaryByParam* attribute ensures that there is a single cached page available for this duration specified.

* VaryByParam can take various "key" parameter names in the query string. There are also other attributes like VaryByHeader, VaryByCustom and so on. Please refer to MSDN for more on this.

2. Fragment Caching

Even though this definition refers to caching portions of a page, it is actually caching a user control that can be used in a base web form page. In theory, if you have used include files in the traditional ASP model then this caching model is like caching these include files separately. In ASP.NET more often this is done through User Controls. Initially even though one feels a bit misleading, this is a significant technique that can be used especially when implementing "n" instances of the controls in various *.aspx pages. We can use the same syntax that we declared for the page level caching as shown above, but the power of fragment caching comes from the attribute "VaryByControl". Using this attribute one can cache a user control based on the properties exposed.

Syntax: <%@ OutputCache Duration="60" VaryByControl="DepartmentId" %>

The preceding syntax, when declared within an *.ascx file, ensures that the control is cached for 60 seconds and the number of representations of the cached control is dependant on the property "DepartmentId" declared in the control.

Add the following into an *.ascx file. Please note the use of the tag "Control" and the cache declaration.

<%@ Control Language="C#"%>
<%@ outputcache duration="60" varybycontrol="DepartMentId" %>

<script runat="server">
private int _Departmentid=0;
public int DepartMentId
{
get{return _Departmentid;}
set{_Departmentid =value;}
}
//Load event of control
void Page_Load(Object sender, EventArgs e)
{
lblText.Text = "Time is " + DateTime.Now.ToString() + " for Department id = "
+ _Departmentid + "\n";
}
</script>
<
asp:Label id="lblText" runat="server"></asp:Label>

Add the following to an *.aspx file. Please note the way the "Register" tag is used; the declaration of the control uses the syntax:

<[TagPrefix]:[TagName]>;

To use the property "DepartMentId". Open the page in two browsers and closely watch the Base form timing and the User control timing. Also note that the following page results in two copies or the representation of the user control in the cache.

<%@ Page Language="C#" Trace="true" %>
<%@ Register TagPrefix="CacheSample" TagName="Text" src="CachingControl.ascx" %>
<script runat=server>
void Page_Load(Object sender, EventArgs e)
{
this.lbltime.Text ="Base form time is " + DateTime.Now.ToString() + "\n";
}
</script>
<
html>
<
head>
</
head>
<
body>
<
form runat="server" ID="Form2">
<
table>
<
tbody>
<
tr>
<
td>
<
asp:Label id="lbltime" runat="server"></asp:Label>
</
td>
</
tr>
<
tr>
<
td>
<
CACHESAMPLE:TEXT id="instance1" runat="Server" DepartMentId="0">
</CACHESAMPLE:TEXT>
</
td>
</
tr>
<
tr>
<
td>
<
CACHESAMPLE:TEXT id="instance2" runat="Server" DepartMentId="1">
</
CACHESAMPLE:TEXT>
</
td>
</
tr>
</
tbody>
</
table>
</form>
</
body>
</
html>

3. Application Level Caching

With Page level Output caching one cannot cache objects among pages within an application. Fragment caching is great in that sense but has limitations when using user controls as the means to do it. We can use the Cache object programmatically to take advantage of caching objects and share the same among pages. Further the availability of various overloaded methods gives a greater flexibility for our Cache policy like Timespan, Absolute expiration and so on. But one of the biggest takes is the CacheDependancy. This means that one can create a cache and associate with it a dependency that is either another cache key or a file.

In almost all Web applications there could be numerous master tables that act as lookups to application specific tables. For example if you take up adding a Employee, usually one has master tables like "tblQualification" to get a list of qualifications, "tblLocations" to get a list of locations and so on. These tables* are usually set during the initial application configuration phase and could be modified once a month or even less than that. Hence it makes sense for us to use them in our Cache rather than making calls to the database on each request. But then what Cache Policy do we adopt?

We cannot hold these objects in the Cache for an entire application instance, because if anybody changes data in these tables one must also refresh the cache. It is here that CacheDependancy can be used.

* Even though these tables are less frequently used for updates, they are extensively used in our select statements throughout the applications.

Find below the snippet that uses CacheDependancy. Here what I have done is to provide a list view of existing employees. You need to create a database in SQL Server then set up some data before you can continue. The schema scripts are enclosed in the article.

Add a database connection value in the Web.Config and change the value as per your setup.

<appSettings>
<
add key="conn" value="Data Source=vishnu;trusted_connection=yes;Initial Catalog=Users"/>
</
appSettings>

First I get the dataset into which I fill the user list. But before this I check for the cache initially if it exists I directly cast it to a dataset, if not create a cache again as in the following:

daUsers.Fill(dsUsers,"tblUsers");

I create the cache with "Users" as the key using Cache.Insert* and link this with a file "Master.xml". This "Master.xml" is a XML file that contains Master data of "tblQualifications" and "tbllocations". I have used "Server.MapPath" to get the physical path of the file on the server. The CacheDependancy instance will ensure that any change in this dependency file means that you need to recreate your cache key definition. This is a great feature to use since I can recreate my cache only when required instead of caching the data at the page level.

Cache.Insert("Users",dsUsers,new System.Web.Caching.CacheDependency(Server.MapPath("Master.xml")) , DateTime.Now.AddSeconds(45),TimeSpan.Zero);

* For other overloaded parameters refer MSDN.

Also note how we could use a trace within to add my own statements.

HttpContext.Current.Trace.Write("from Database..");

<%@ Page Language="c#" Trace="true" %>
<%@ import Namespace="System" %>
<%@ import Namespace="System.Data" %>
<%@ import Namespace="System.Data.SqlClient" %>
<%@ import Namespace="System.Configuration" %>
<%@ import Namespace="System.Web" %>
<%@ import Namespace="System.Collections" %>
<%@ import Namespace="System.IO" %>
<script runat="server">
void Page_Load(Object sender, EventArgs e)
{
DataSet dsUsers;
try
{
if(Cache["Users"]==null)
{
SqlConnection cn;
dsUsers =
new DataSet("new");
cn =
new SqlConnection(ConfigurationSettings.AppSettings.Get("conn"));
SqlDataAdapter daUsers;
daUsers =
new SqlDataAdapter("Select * from tblUsers",cn);
cn.Open();
daUsers.Fill(dsUsers,"tblUsers");
//Update the cache object
Cache.Insert("Users",dsUsers, new System.Web.Caching.CacheDependency(
Server.MapPath("Master.xml")), DateTime.Now.AddSeconds(45),TimeSpan.Zero);
HttpContext.Current.Trace.Write(DateTime.Now.AddSeconds(45).ToString() + "
is expiry time..");
cn.Close();
cn.Dispose();
HttpContext.Current.Trace.Write("from Database..");
lblChange.Text ="From the database....";
}
else
{
HttpContext.Current.Trace.Write("From cache..");
lblChange.Text ="From the cache....";
dsUsers= (DataSet) Cache["Users"];
}
dlUsers.DataSource =dsUsers;
dlUsers.DataMember = dsUsers.Tables[0].TableName ;
//lblChange.Text += Server.MapPath("Master.xml");
this.DataBind();
}
catch(Exception ex)
{
lblChange.Text = ex.Message;
}
}

</script>
<!
DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<
html>
<
head>
<
title>Cache Dependency Tester</title>
<meta content="Microsoft Visual Studio 7.0" name="GENERATOR" />
<
meta content="C#" name="CODE_LANGUAGE" />
<
meta content="JavaScript" name="vs_defaultClientScript" />
<
meta content="http://schemas.microsoft.com/intellisense/ie5" name="vs_targetSchema" />
</
head>
<
body ms_positioning="GridLayout">
<
form id="Form1" method="post" runat="server">
<
asp:DataList id="dlUsers" style="Z-INDEX: 101; LEFT: 44px; POSITION: absolute; TOP: 104px" runat="server" Height="148px" Width="343px" BorderWidth="1px" GridLines="Horizontal" CellPadding="4" BackColor="White" ForeColor="Black" BorderStyle="None" BorderColor="#CCCCCC">
<
SelectedItemStyle font-bold="True" forecolor="White" backcolor="#CC3333"></SelectedItemStyle>
<
FooterStyle forecolor="Black" backcolor="#CCCC99"></FooterStyle>
<
HeaderStyle font-bold="True" forecolor="White" backcolor="#333333"></HeaderStyle>
<
ItemTemplate>
<
table>
<
tr>
<
td>
<%#DataBinder.Eval(Container.DataItem,"UserId")%></td>
<
td>
<%#DataBinder.Eval(Container.DataItem,"FirstName")%></td>
<
td>
<%#DataBinder.Eval(Container.DataItem,"LastName")%></td>
</
tr>
</
table>
</
ItemTemplate>
</
asp:DataList>
<
asp:Label id="lblChange" style="Z-INDEX: 102; LEFT: 46px; POSITION: absolute; TOP: 63px" runat="server" Height="28px" Width="295px"></asp:Label>
<
asp:Button id="btnMaster" style="Z-INDEX: 103; LEFT: 50px; POSITION: absolute; TOP: 293px" onclick="btnMaster_Click" runat="server" Text="Refresh Master"></asp:Button>
</
form>
</
body>
</
html>

We created the page that initiates and uses the Cache. For testing purpose we need another page that will overwrite this "Master.xml" on the click of a button for which the code snippet is as follows. This ideally should be our master maintenance page that adds/updates Master records in the database and overwrites the XML. But to make it easy I have just written an overwriting sample.

<%@ Page Language="C#" Trace="true"%>
<%@ import Namespace="System" %>
<%@ import Namespace="System.Data" %>
<%@ import Namespace="System.Data.SqlClient" %>

<script runat="server">
void btnMaster_Click(Object sender, EventArgs e)
{
//Call save function
this.Save();
}
void Save()
{
try
{
SqlConnection cn;
DataSet dsUsers =
new DataSet("Users");
//I have used this to get the Connectionstring from the
//Configuration file.
cn = new SqlConnection(ConfigurationSettings.AppSettings.Get("conn"));
SqlDataAdapter daQualification;
SqlDataAdapter daLocations;
daQualification =
new SqlDataAdapter("Select * from tblqualifications",cn);
daLocations =
new SqlDataAdapter("Select * from tblLocations",cn);
cn.Open();
daQualification.Fill(dsUsers,"tblqualifications");
daLocations.Fill(dsUsers,"tblLocations");
HttpContext.Current.Trace.Write("Masters data up..");
//Overwrite the XML file. Also please read MSDN on the overloaded parameters for WriteXml
dsUsers.WriteXml(HttpContext.Current.Server.MapPath
"Master.xml"),XmlWriteMode.WriteSchema);
cn.Close();
cn.Dispose();
}
catch(Exception ex)
{
throw new Exception(ex.Message);
}
}

</script>
<
html>
<
head>
</
head>
<
body>
<
form runat="server" ID="Form1">
<
span>
<table>
<
tbody>
<
tr>
<
td>
<
label id="lblRefresh" runat="server">
Rewrite the XML File by clicking the buttonbelow.</label> 
</td>
</
tr>
<
tr align="middle">
<
td>
<
asp:Button id="btnMaster" onclick="btnMaster_Click" runat="server"
Text="Write XML"></asp:Button>
</
td>
</
tr>
</
tbody>
</
table>
</
span>
</
form>
</
body>
</
html> 

Now once you have created the preceding pages, in other words one that implements caching and another that overwrites the dependency file, create two instances of yur browser. Open the cache implementation page and note for trace, label text. Open the other instance of the browser with the page that overwrites the XML. Note that in the former, the first time it fetches data from the database and the subsequent request will be from the cache until your expiration time of 45 seconds is reached or anyone overwrites or changes the "Master.xml" file. Also give a look at Timespan parameter since you have a concept of a Sliding expiration that can also be implemented. Keep refreshing the first page and you will see that the trace indicates the cached page retrieval. Click the overwrite XML button on the latter page that would overwrite the XML and again refresh the former page to note that the data is retrieved from the database. Though in this example I have not shown any direct relation between the cached data and the dependency file (like get values from the dependency file and merge with the cached object and so on) in terms of integrated usage, this could very easily be designed and implemented. Dependency caching is a powerful technique that .NET supports and should be utilized wherever applicable.

Conclusion

Caching is a technique that definitely improves the performance of web applications if one is careful to have a balance in terms of which data needs to be cached and parameter values for expiration policy.


Similar Articles