Search This Blog

Tuesday, September 21, 2010

Caching in ASP.NET

Caching is one of the coolest features in Asp.net. Caching enables you to store the expensive data into Cache object and later retrieve it without doing expensive operations. Caching can save a lot of expensive operations since you can store all the records in the cache object and use the cache object as the data source.
ASP.NET provides the flexibility in terms of caching at different levels:
1. Page Level Output Caching
2. Fragment Caching
3. Application Level Caching

Output Caching:

Output caching is used for pages and is also known as Page-level caching. All you need to do to enable output caching is to add a directive in your html view of the aspx page.
In order to cache a page's output, we need to specify an @OutputCache directive at the top of the page. The syntax is as shown below:

<%@ OutputCache Duration=5 VaryByParam="None" %>
 
As you can see, there are two attributes to this directive. They are:
  • Duration - The time in seconds of how long the output should be cached. After the specified duration has elapsed, the cached output will be removed and page content generated for the next request. That output will again be cached for 10 seconds and the process repeats.
  • VaryByParam - This attribute is compulsory and specifies the querystring parameters to vary the cache. In the above snippet, we have specified the VaryByParam attribute as None which means the page content to be served is the same regardless of the parameters passed through the querystring [see Example 1 in the sample download].
    If there are two requests to the same page with varying querystring parameters, e.g.:

    .../PageCachingByParam.aspx?id=12 and .../PageCachingByParam.aspx?id=15] and separate page content is generated for each of them, the directive should be:



    • <%@ OutputCache Duration=5 VaryByParam="id" %>
      The page content for the two requests will each be cached for the time specified by the Duration attribute [see Example 2 in the sample download].
      To specify multiple parameters, use semicolon to separate the parameter names. If we specify the VaryByParam attribute as *, the cached content is varied for all parameters passed through the querystring.
    Some pages generate different content for different browsers. In such cases, there is provision to vary the cached output for different browsers. The @OutputCache directive has to be modified to:


    <%@ OutputCache Duration=5 VaryByParam="id" VaryByCustom="browser" %>
     
    This will vary the cached output not only for the browser but also its major version. I.e., IE5, IE 6, Netscape 4, Netscape 6 will all get different cached versions of the output.


    Programmatic Page Caching

    You can also use caching programmatically, meaning that you can change the value of cache depending upon the tasks performed by the user. The Response.Cache class let's you access the functionality to work with the cache object.

    You can change the expiration time on the Cache using the SetExpires method of the Response.Cache class.

    Response.Cache.SetExpires(System.DateTime.Now.AddMinutes(10));

    In the same way you can also use Response.Cache.VaryByParams to set the Params programmatically.


    Caching page fragments

    Sometimes we might want to cache just portions of a page. For example, we might have a header for our page which will have the same content for all users. There might be some text/image in the header which might change everyday. In that case, we will want to cache this header for a duration of a day.
    The solution is to put the header contents into a user control and then specify that the user control content should be cached. This technique is called fragment caching.

    To specify that a user control should be cached, we use the @OutputCache directive just like we used it for the page.


    <%@ OutputCache Duration=10 VaryByParam="None" %>
    With the above directive, the user control content will be cached for the time specified by the Duration attribute [10 secs]. Regardless of the querystring parameters and browser type and/or version, the same cached output is served.

    Accessing Caching in the Class Libraries

    Sometimes you will need to access the caching object in the class library. You cannot use Response class anymore since its only limited to the asp.net code behind page. For accessing cache in class library you will have to use HttpContext.Current.Cache. Also don't forget to include System.web namespace.

    Data Caching

    ASP.NET also supports caching of data as objects. We can store objects in memory and use them across various pages in our application. This feature is implemented using the Cache class. This cache has a lifetime equivalent to that of the application. Objects can be stored as name value pairs in the cache. A string value can be inserted into the cache as follows:


    Cache["name"]="Smitha";
    The stored string value can be retrieved like this:


    if (Cache["name"] != null)
        Label1.Text= Cache["name"].ToString();
    [See example 4 for an illustration.]
    To insert objects into the cache, the Add method or different versions of the Insert method of the Cache class can be used. These methods allow us to use the more powerful features provided by the Cache class. One of the overloads of the Insert method is used as follows:


    Cache.Insert("Name", strName, 
        new CacheDependency(Server.MapPath("name.txt"), 
        DateTime.Now.AddMinutes(2), TimeSpan.Zero);
    The first two parameters are the key and the object to be inserted. The third parameter is of type CacheDependency and helps us set a dependency of this value to the file named name.txt. So whenever this file changes, the value in the cache is removed. We can specify null to indicate no dependency. The fourth parameter specifies the time at which the value should be removed from cache. [See example 5 for an illustration.] The last parameter is the sliding expiration parameter which shows the time interval after which the item is to be removed from the cache after its last accessed time.
    The cache automatically removes the least used items from memory, when system memory becomes low. This process is called scavenging. We can specify priority values for items we add to the cache so that some items are given more priority than others:


    Cache.Insert("Name", strName, 
        new CacheDependency(Server.MapPath("name.txt"), 
        DateTime.Now.AddMinutes(2), TimeSpan.Zero, 
        CacheItemPriority.High, null);
    The CacheItemPriority enumeration has members to set various priority values. The CacheItemPriority.High assigns a priority level to an item so that the item is least likely to be deleted from the cache.

    Application Level Caching

    With Page level Output caching one cannot cache objects between pages within an application. Fragment caching is great in that sense but has limitations by using user controls as means to do. We can use the Cache object programmatically to take advantage of caching objects and share the same between pages. Further the availability of different overloaded methods gives a greater flexibility for our Cache policy like Timespan, Absolute expiration etc. 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 e.g. if you take up adding a Employee, usually one has master tables like "tblQualification" to get list of qualifications, "tblLocations" to get list of locations etc. 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 database on each request. But then what Cache Policy do we adopt?

    We cannot hold these objects in Cache for entire application instance, because if anybody changes data in these tables one has to 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 through out 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, setup some data before you can continue. The schema scripts are enclosed in the article.
    Add database connection value in 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.
    daUsers.Fill(dsUsers,"tblUsers");
    I create the cache with "Users" as 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 make sure 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 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 objectCache.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 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 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 functionthis.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 WriteXmldsUsers.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 above pages i.e. one that implements caching and other that overwrites the dependency file, create two instance of browser and open the cache implementation page and note for trace, label text; open the other instance of browser with the page which overwrites the XML. Note the former, the first time it fetches data from the database and the subsequent request will be from cache till your expiration time of 45 seconds is reached or anyone overwrites or changes the "Master.xml" file. Also give a look on Timespan parameter since you have a concept of Sliding expiration that can also be implemented. Keep refreshing the first page and you will see that 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 database. Though in this example I have not shown any direct relation between the cached data and the dependency file (like get values from dependency file and merge with cached object etc) 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.

    Points of interest

  • If there are old ASP pages in your website which use the Response.Expires property to cache page output, they can be retained as such. ASP.NET supports this property as well.
  • The Insert method of the Cache class will overwrite any existing item with the same key name.
  • The CacheItemPriority.NotRemovable priority value can be used with Cache.Insert method to set the priority level of an item so that the item will not be removed from the cache during scavenging.


     

No comments:

Post a Comment