Data Caching in ASP.NET

If you use static data with your ASP.NET applications, such
as that from a database server like Microsoft SQL Server, you should take a
look at caching your data. Previously, this was not an easy thing to do, but the
caching in .NET makes caching your data and objects a trivial task. This
tutorial will cover basic caching and scenarios where caching would be good to
use.

In ASP.NET, every page you write extends the
System.Web.UI.Page class, which contains a member called Cache. You use the
Cache property as you would any other IEnumerable, which gives you methods and
properties to get, set, and enumerate through members. We will be looking at
getting and setting items in the cache. In the simplest example, you can
retrieve any object from the cache like so:

Object obj = (Object)Cache["key"];

What this does is look in the Cache for an item with the key "key".
If such an item exists, the object is returned. While we don't need to cast the
returned object to Object, this is just an example of how you must cast the
return to the class type you want returned. If the item is not found, null is
returned, so you will need to do some checking:

Object obj = (Object)Cache["key"];
if (obj == null) {
 // Generate a new object and insert it into the cache
}
// Use your object

You can also assign object to the cache like the following code example, but
using Cache.Inset() is a much better means as you'll see later:

Cache["key"] = obj;

Caching in action- an example

Now let's look at a serious example. In the example below, we will define a
function that binds data to a control, such as a ASP.NET DataGrid control. We
will look at data binding in a little more depth later. For this example, we
will use a System.Data.DataSet object.

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<script runat="server">
private string _filename = "mydata.xml";

private void Page_Load(Object src, EventArgs args) {
DataSet ds = (DataSet)Cache["mydata"];
if (ds == null) {
ds.ReadXml(Server.MapPath(_filename));
Cache.Insert("mydata", ds,
new CacheDependency(Server.MapPath(_filename)),
DateTime.Now.AddHours(12), NoSlidingExpiration);
}

myDataGrid.DataSource = ds;
myDataGrid.DataBind();
}

We'll break down the above example. The first part we've already discussed,
where we get the object from the cache. If the object is null (i.e., doesn't
exist in the cache
), we need to create the object. The first line after
checking if our DataSet object is null loads an XML file called a DiffGram,
which is a serialized DataSet object. We'll talk about this later. Essentially,
the DataSet object (hereforth, "ds") is created from serialized
XML, which you can event type by hand. We call Server.MapPath() because the file
as we know it is virtual. ds.ReadXml needs the absolute path and filename to
load it. Server.MapPath() figures out what the absolute path of a virtual
filename (such as your web pages) and returns the absolute path.



The next statement inserts the newly created "ds" into the cache.
Again, the statement is:

Cache.Insert("mydata", ds,
 new CacheDependency(Server.MapPath(_filename)),
 DateTime.Now.AddHours(12), NoSlidingExpiration);
  1. The first parameter is the key used to identify our cache item.
  2. The second parameter is the object we want to cache. In this example, we
    are caching the DataSet object "ds". This can be any object,
    however, including strings, numbers, controls, etc.
  3. The third parameter creates a dependency on an object. If this object
    changes, the cached object is marked as expired and your application will
    regenerate next time, returning null as your object reference. In fact, the
    object is expired. You can also pass null in this parameter to signify that
    you don't want any dependencies.
  4. The fourth parameter is the absolute expiration period your object is
    valid. Here, we use DateTime.Now (which uses "now's" time) and add
    12 hours to it. This means the object will automatically expire in 12 hours.
    You could also use NoAbsoluteExpiration which means the object will never
    expire until the dependent object changes. We use an absolute expiration
    here to make sure we have somewhat recent data in case the CacheDependency
    doesn't expire the data for some reason. (As if Microsoft software doesn't
    have bugs!)
  5. The fifth and final parameter is a sliding expiration. This means that if
    the object is access, the absolute expiration time is essentially pushed
    back to however many minutes, hours, etc. you specify here. This is an
    object of type TimeSpan. Here, we want our object expired after 12 hours
    even if the object is accessed before then.

Binding our object to a data-driven control

After all this, we can finally bind our object to a data-driven control, such as
the ASP.NET DataGrid control. While the full use of the control is out of the
scope of this tutorial, I will cover the usage in brief detail.

The DataGrid control is a data-driven control, meaning that on the server, the
control will use data and will then render the HTML onto the page for the client
to see in their browser. We will define a basic DataGrid control customizing
only a few parameters:

<body>
<asp:DataGrid id="myDataGrid" runat="server"
 AutoGenerateColumns="true"
 ShowHeader="true"
 AllowPaging="true"
 PageSize="25"
 Font-Name="Verdana"
 Font-Size="10pt">
 <HeaderStyle Font-Bold="true"/>
</asp:DataGrid>
</body>

Please note: this example assumes you already have your other HTML tags
setup and that the DataGrid is the only control in the BODY, which it doesn't
have to be.

Above, we define a Web server control of type DataGrid, which is a data-driven
control that renders a table on the page. By setting AutoGenerateColumns to true
(which is default anyway), we specify that the DataGrid control will
automatically generate headers and columns based on your data in the order of
the columns in your DataSet. We also setup default paging and a few items
styles, while making the Header column text bold.



In the example Page_Load() event handler, we did a little data binding like so:

myDataGrid.DataSource = ds;
myDataGrid.DataBind();

These two lines associate the DataSource property of the DataGrid with our
DataSet object "ds" and then binds the data with DataBind(). If you do
not call DataBind(), you will find that nothing is rendered on your page. The
DataSource property can point at other types of object, like ArrayList(),
DataView() and many others. Consult your .NET SDK Documentation for more
details.

Strategies for caching your ASP.NET site

Why would you use this tutorial to build your next data-driven ASP.NET page?
There are several examples, but I will discuss one here. Lets say that you've developed a page or site that generates a table of contents
on every page. You also have a SQL Server storing this data because the table of
contents changes from time to time. Instead of making calls across the network
to the SQL Server (even cached data on SQL Server requires a few calls back and
forth), you could use the caching example above.

There are two ways primary ways of handling this:

  1. Setup a scheduled task on the SQL Server 2000 to output your table as XML
    to a specified location on your web server. Because SQL Server 2000 can
    serialize your data to DiffGrams used in DataSet construction, this is an
    easy way to deliver updates to your DiffGram file described above in our
    caching example. You could even setup a trigger on your SQL Server to
    generate a new DiffGram when the contents of your table change, but that is
    beyond the scope of this tutorial.
  2. Another way is to adapt the example above to create not only a DataSet,
    but to use classes from System.Data.OleDb or System.Data.SqlClient to get
    your data from time to time from the server. In this particular case (which
    we'll do below), creating a cache dependency wouldn't make much sense, because the SQL Server is no longer updating your DiffGram file, identified
    by the private member _filename in the example above.

You could adapt second method above to something like the following:

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Data" %>
<script runat="server">
// If used frequently, store these strings in your Web.config
// and use ConfigurationSettings.AppSettings["key"] to get them out.
// If you do this, make sure these are constant strings!
private const string connstr = "Data Source=sqlserver1;Initial Catalog=Web";
private const string cmdstr = "SELECT Title,URL FROM TOC";

private void Page_Load(Object src, EventArgs args) {
DataSet ds = (DataSet)Cache["mydata"];
if (ds == null) {
// Connect to the SQL Server and get our data using a DataAdapter
SqlConnection conn = new SQlConnection(connstr);
SqlCommand cmd = new SqlCommand(cmdstr, conn);
SqlDataAdapter adapter = new SqlDataAdapter(cmd);

// Create a new DataTable and fill it
DataTable dt = new DataTable("Item");
adapter.Fill(dt);

// Now add our DataTable to a DataSet instance and add to cache
ds = new DataSet("TOC");
ds.Tables.Add(dt);
Cache.Insert("mydata", ds, null, DataTime.Now.AddHours(1), NoSlidingExpiration);
}

// Bind our data
myRepeater.DataSource = ds;
myRepeater.DataBind();
}

Yes, I've chanced our ASP.NET control to a Repeater because a DataGrid
wouldn't make a lot of sense. I will describe what you could use in this example
for a Repeater control in a minute.

Describing the example above, it is really nothing more than what we had before
execpt that we have no CacheDependency because we have no files to watch, and we
create our DataSet from a DataTable that is filled by a SqlDataAdapter (or
OleDbDataAdapter if you prefer). DataSets are good to store in the Cache since
they are easy to use, can be used in any data-driven control, and can be
serialized at will with one call to ReadXml() or WriteXml().

Note: another thing to mention to those that have used System.Data.OleDb
classes but not System.Data.SqlClient classes, you will notice I didn't specify
a Provider in the connection string. This is because the SqlClient classes are
only interested in SQL Server connections and, hence, already know what the
Provider is.

Finally, our Repeater control would look something like the following:

<body>
<asp:Repeater id="myRepeater" runat="server" Visible="true">
<ItemTemplate>
<dd><a href="<%# DataBinder.Eval(Container, "DataItem.URL") %>">
<%# DataBinder.Eval(Container, "DataItem.Title") %></a></dd>
</ItemTemplate>
</asp:Repeater>
</body>

The expressions above evaluate your Container (your DataSet object) and find
the "DataItem.Title" and "DataItem.URL". "Item" is
the name of the data item, which is the the name of the table usually. You may
remember that we called our DataTable "Item" above:

DataTable dt = new DataTable("Item");

"Title" and "URL" are the names of our fields in the table.
See, even your data is appropriately named!

Conclusions

That pretty much wraps up the tutorial of how to use caching. There are many
other examples and scenarios of how to use Caching and DataSets to cache data,
but this tutorial is long enough I think. Please keep in mind that you can cache
other objects as well, not just DataSets. I find that caching DataSets where you
used infrequently accessed data is especially helpful, however, to decrease
loads on your Web server, SQL Server (or other database server), and network.

I hope you've found this tutorial helpful and that the wheels in your head are
spinning like mad with new ways to use caching to improve your web site's
performance!