.Net Knowledge Nuggets: Data Caching

When writing ASP.Net applications, you often have a want/need to cache data in the UI layer for reuse. Often this is used to improve performance (limit repeated database calls, for example) or store process state. Below is an overview of various ways to achieve this for various scenarios.

Executive Summary:

(ordered from shortest to longest typical duration)

MethodScopeLifespanWhen to Use
ViewStateCurrent page/control and user (each page/control has its own viewstate)Across post-backYou need to store a value for the current page request and have it retrieved on the next post-back.
Base ClassCurrent page/user and it's child controlsCurrent instanceYou need to store a value once per page request  (such as values from a database) and have access to it during the current page request only
HttpContextCurrent page/user and it's child controlsCurrent instanceYou need to store a value once per page request  (such as values from a database) and have access to it during the current page request only
ASP SessionSite-wide, current user, all pages/controlsDuration of user's sessionYou need to store a value once per user's visit to the site (such as user profile data) and have access to it from any code for the duration of the user's visit
ASP CacheSite-wide, All pages/controls, all usersUntil expires or server restartsYou need to store data for access from any code for all users (such as frequently used, but rarely changed, database values -- such as a list of Countries for an address form).
CookiesSite-wide, current user, all pages/controlsUntil expires or browser deletesYou need to store small data (such as user's uniqueId) from one visit to the next, or possibly across sites. Not for sensitive data!

ViewState:

If you’re an ASP.Net developer, you should have a firm grasp of ViewState and all its benefits and drawbacks. Basically, ViewState allows you to store data in a special hidden input field which is provided back to you when the user posts-back the page. This is similar to just using a hidden field, except that it is page-/control-specific (meaning, if you have a user control that is repeated on the page, each instance of the control can store its own ViewState data with the same key and get back its individual results). It also includes some basic security  and compression.

// Set ViewState value while rendering the page
protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);
 
    // Set ViewState value
    this.ViewState.Add(“MyComputedValue”, (IList)BLL.DoSomeComputationThatShouldOnlyRunOnce()); 
}
 
// After post-back, retrieve the value
protected void Page_Load(object sender, EventArgs e)
{
 
    IList myValue = (IList)this.ViewState[“MyComputedValue”];
    if (myValue == null)
    {
        myValue = BLL.DoSomeComputationThatShouldOnlyRunOnce();
    }
 
}

I would discourage the use of ViewState for storing anything more than just very small pieces of data, since this information is included in the rendered HTML and has to be downloaded/uploaded with each request, thus degrading performance. You can configure Viewstate to use Session for it’s storage and eliminate the need to include it in the page’s HTML, but if you’re going that route, why not just use Session directly for your caching location (see below)?

BasePage (shared base class):

A common pattern I’ve used on just about every project and highly suggest it for many reasons, is to have a "BasePage” class which inherits System.Web.UI.Page, then have all of my application pages inherit from BasePage. This allows the developers to create shared "shortcuts” in one location which are accessible from all of our UI layer code.

Among other useful shortcuts (like storing singletons, etc), you can create properties on your BasePage class for storing cached data during the current page invocation. For instance, if you’re using the ASP.Net membership providers, you can store the current authenticated user in your BasePage so that you’re not going to the database everytime you call Membership.GetUser()

Note too, that this pattern can be combined with the other patterns listed, such as having a property that reads/writes data from Session, Cookies, etc., allowing for reduced code duplication.

using System.Web;
using System.Web.Security;
 
namespace MyProject.WEB
{
    public abstract class MyBasePage : System.Web.UI.Page
    {
    
        /// 
        /// Cached reference to Membership.GetUser(); (Currently authenticated user, or null if not auth’d)
        /// From Membership.GetUser():
        /// Gets the information from the data source and updates the last-activity date/time stamp for the current logged-on membership user.
        ///

        /// A System.Web.Security.MembershipUser object representing the current logged-on user.
        internal MembershipUser AuthenticatedUser
        {
            get
            {
                if (_authedUser == null)
                {
                    _authedUser = Membership.GetUser();
                }
                return _authedUser;
            }
        }
        private MembershipUser _authedUser;
    }
}

To follow this further, you can create a ControlBase class for your user controls which has a typed reference to the BasePage:

namespace MyProject.WEB
{
    public abstract class MyControlBase : System.Web.UI.UserControl
    {
        protected MyPageBase BasePage
        {
            get { return (MyPageBase)Page; }
        }
    }
}

Now, from within your control, you can do this.BasePage.UserName to get the currently logged-in username without having to go to the database more than once per page rendering.

HttpContext:

You can use the HttpContext Items array to store values for the duration of the current page rendering (similar to the PageBase pattern above). Personally, I prefer using the PageBase pattern, but there are some cases where this isn’t possible, such as when your working within a CMS framework like SiteCore and don’t actually have access to the page. (SiteCore only allows you to create user controls and place them via their CMS framework).

/// 
/// Cached reference to Membership.GetUser(); (Currently authenticated user, or null if not auth’d)
/// From Membership.GetUser():
/// Gets the information from the data source and updates the last-activity date/time stamp for the current logged-on membership user.
///

/// A System.Web.Security.MembershipUser object representing the current logged-on user.
internal MembershipUser AuthenticatedUser
{
    get
    {
        if (HttpContext.Current.Items[“CurrentUser”] == null)
        {
            HttpContext.Current.Items[“CurrentUser”] = Membership.GetUser();
        }
        return (MembershipUser)HttpContext.Current.Items[“CurrentUser”];
    }
}
 

ASP.Net Session:

Using ASP.Net Session will provide a way to store data across page views for the duration of the user’s visit to the site. Be careful – I’ve seen many people get tangled up with stale session data, particularly on initial page loads. For instance, a user clicks on a button which will initiate an Add/Edit popup in edit mode for a product.  The developer stores the product info in Session, then opens the popup control, which checks Session for a product and goes into edit mode if product data exits.  The user changes their mind and closes the popup (but the developer forgets to clear the product from session in this case).  Then the user clicks the "Add new product" button, showing the same control which should be in "add" mode, but since there is a stale product in session, it enters edit mode for the previous product instead.   Make sure that if a user is returning to a page after previously storing page state in session that you correctly handle the potentially stale data.

public SortDirection LastSortDirection
{
    get
    {
        //Note: will return null if no value exists in Session
        return (SortDirection)HttpContext.Current.Session[“SortDir”];
    }
    set
    {
        HttpContext.Current.Session[“SortDir”] = value;
    }
}

ASP.Net Cache:

The ASP.Net Cache can be used to store objects for a predetermined amount of time across all page requests (ie: at the server level). This is useful for data read from the database that isn’t often changed, such as a list of options for a drop-down list.

internal List<String> DropDownListOptions
{
    get
    {
        if (HttpRuntime.Cache[“DropDownListOptions”] == null)
        {
            HttpRuntime.Cache.Insert(“DropDownListOptions”, DAL.GetListFromDatabase(), null, DateTime.Now.AddHours(24), System.Web.Caching.Cache.NoSlidingExpiration);
        }
        return (List<String>)HttpRuntime.Cache[“DropDownListOptions”];
    }
}

Cookies:

Cookies can be used to save data on the client side and have it returned to you on postback. Note, however, that unlike the other storage mechanisms, cookies have two different storage locations: one for the inbound value and one for the outbound value. So you can’t (at least, not without some additional logic) write a value, then read it again for use later in your page logic (your "read" will just re-get the original value, not the updated value). Generally, I would suggest reading the value at page load, storing it in a property on your page class, then writing it out again in your PreRender code.

Also note that not setting a cookie value on your response is not the same as deleting the cookie. The browser will keep the last cookie received until it expires or is explicitly overwritten.

Warning: Cookies are stored on the user’s machine, so don’t store sensitive data there and always validate the values you get back (it’s easy to tamper with the values). Encryption is suggested, as is setting the ".Secure” property to restrict transport to HTTPS.

private const string COOKIE_NAME = “MyCookie”;
 
/// 
/// Update the cookie, with expiration time a given amount of time from now.
///
public void UpdateCookie(List<KeyValuePair<string, string>> cookieItems, TimeSpan? cookieLife)
{
    HttpCookie cookie = Request.Cookies[COOKIE_NAME] ?? new HttpCookie(COOKIE_NAME);
    
    foreach (KeyValuePair<string, string> cookieItem in cookieItems)
    {
        cookie.Values[cookieItem.Key] = cookieItem.Value;
    }
    
    if (cookieLife.HasValue)
    {
        cookie.Expires = DateTime.Now.Add(cookieLife.Value);
    }
    Response.Cookies.Set(cookie);
 
}
 
public string ReadCookie(string key)
{
    string value = string.Empty;
    
    if (Request.Cookies[COOKIE_NAME] != null)
    {
        value = Request.Cookies[COOKIE_NAME].Values[key];
        //UpdateCookie(cookieName, value); //optional: update the expiration so it rolls outward
    }
    
    return value;
}
 
public void DeleteCookie()
{
    var cookie = new HttpCookie(COOKIE_NAME)
    {
        Value = string.Empty,
        Expires = DateTime.Now.AddDays(-1)
    };
    Response.Cookies.Set(cookie);
}