Monday, March 18, 2013

Cache Multiple Versions of a user control using VaryByParam, VaryByControl ,and VaryByCustom


Recently, I was working on optimization of application performance where we were using some user control which was taking a lot of time to load. It contains a user role based information as authenticated and authorized users are able to see some links and portion of pages.

Initially, I thought, as it is user specific storage, storing Session will be good idea but didn't find any such way to store data in Session like OutputCache. Hence decided to use "User specific Control Cache" like Session.

Here are the following ways that we can achieve it:

VaryByParam:

VaryByParam works based on parameters passed in URL's query string. It gets a list of query string or form POST parameters that the output cache will use to vary the user control.

<%@ OutputCache Duration="120" VaryByParam="PageTitle" %>

Example and few points to note:

1. If you url is http://www.xyz.com?PageTitle=Home, it will cache control for that patricular url for specified duration.

2. If you move to anyother page like "Contact Us", it will cache different version of same user control.

3. If user has multiple parameters in query string/Form posting, again multiple version of control are cached. E.g if you want to cache control based on User ID, add parameter as follows:

<%@ OutputCache Duration="120" VaryByParam="PageTitle;UserID" %>

Note
You can pass authenticated token in query string as a part of UserID, as SessionID/ other user PII information is not recommended to pass in URL as a plain text.


VaryByControl:

You can cache multiple versions of a user control by simply declaring it in an .aspx file more than once. As with user controls that are not cached, you can include a cached user control in an ASP.NET page as many times as needed for your application. Unless you set the Shared property to true for the user control, multiple versions of the control output will be stored in the cache.

How to create:

1. Create a control that post-backs itself.

2. To cache the user control based on user control properties, you should specify the fully qualified name of the properties in the varyByControls section of the PartialCachingAttribute. Multiple properties if any should be separated by semi-colons.


<%@ Control Language="C#" AutoEventWireup="true" 
CodeFile="WebUserControl.ascx.cs" 
Inherits="WebUserControl" %>
<%@ OutputCache Duration="60" 
VaryByControl="WebUserControl.param1;WebUserControl.param2
VaryByParam="none" Shared="true" %>


or you can also include the PartialCache attribute for the user control:

[PartialCaching(60, null, "WebUserControl.param1;WebUserControl.param2", null, true)]
public partial class WebUserControl : System.Web.UI.UserControl
{
    public string param1 { get; set; }
    public string param2 { get; set; }
}


OR another way to cache the control on the combination of both values would be:

[PartialCaching(60, null, "WebUserControl.BothParams", null, true)]
public partial class WebUserControl : System.Web.UI.UserControl
{
    public string param1 { get; set; }
    public string param2 { get; set; }

    public string BothParams    
    {
        get { return String.Concat(param1, param2); }
    }

}

The last parameter (true) specifies shared. Duration is specified by 60. Refer to the link How to: Cache Multiple Versions of a User Control Based on Parameters

Assign it in the user control code behind:




[PartialCaching(60, null, "WebUserControl.BothParams", null, true)]
public partial class WebUserControl1 : System.Web.UI.UserControl
{
    ...
    protected void Page_Load(object sender, EventArgs e)
    {
        this.CachePolicy.Duration = new TimeSpan(0, 0, 60);
    }    
}
You can assign it in the code behind of the page where user control is referenced using the ID of the user control.

e.g. If the user control on the aspx is:

<mycontrols:control1 ID="ucControl1" runat="server" param1="15" param2="20" />

then in the code behind of aspx, you should write:

this.ucControl1.CachePolicy.Duration = new TimeSpan(0, 0, 60);


NOTE
if both the user control and page are cached: If the page output cache duration is less than that of a user control, the user control will be cached until its duration has expired, even after the remainder of the page is regenerated for a request. For example, if page output caching is set to 50 seconds and the user control's output caching is set to 100 seconds, the user control expires once for every two times the rest of the page expires.


VaryByCustom:

This one is my favorite. You can play with customized values. The key of creating a custom cache variance is understanding that ASP.NET uses a simple string comparison to determine if a cached result should be returned instead of processing the page. For example, say we want to cache a certain page by SessionID. We add the OutputCache directive like this:

<%@ OutputCache Duration=”60” VaryByParam=”None” VaryByCustom=”SessionID” %>


Now, in global.asax, we must override the GetVaryByCustomString method, like this:


Public override string GetVaryByCustomString(HttpContext context, string arg) 

  if(arg.ToLower() == “sessionid”) 
  { 
    HttpCookie cookie = context.Request.Cookies[“ASP.NET_SessionID”]; 
    if(cookie != null) 
      return cookie.Value; 
  } 
  return base.GetVaryByCustomString(context, arg); 
}

In case of multiple keys, you can supply multiple values separated by ';' and handle same in GetVaryByCustomString method as given below:

<%@ OutputCache Duration=”60” VaryByParam=”None” VaryByCustom=”SessionID;Key1” %>


public override string GetVaryByCustomString(HttpContext context, string custom)
        {
            string[] keys = custom.Split(new char[] { ';' });
            string result = string.Empty;
         
            foreach (string key in keys)
            {

                switch (key)
                {
                    case "Key1":
                        //Add page Url
                        result = //Add Key1 logic.
                        break;
                    case "SessionID":
                          HttpCookie cookie = context.Request.Cookies["ASP.NET_SessionID"];
                        result += cookie.Value;
                        break;
                }
            }

            if (!string.IsNullOrEmpty(result))
            {

                return result;
            }
            else
            {

                return base.GetVaryByCustomString(context, custom);
            }
        }



That’s it. Simple, elegant, beautiful :)