1-888-365-2779
+1-888-365-2779
Try Now
More in this section
Categories
Bloggers
Blogs RSS feed

Performance Optimizations Part 2 – Cache Substitution Controls

by Georgi Chokov

In the previous article “Performance Optimizations - Why Output Cache is Important” we saw a brief overview of output caching and how it can help overall performance and experience of our sites. We also narrowed down possible problems that output caching may impose in certain scenarios.

In this article we will talk about cache substitution: what problems it solves, how it works, how Sitefinity makes substitution implementations easier and when to use it.


What is cache substitution? 

Cache substitution allows us to inject HTML into already cached output at predefined points. This way we can spare CPU time by rendering only small parts of the page that need frequent updates.


How it works?

ASP.Net provides HttpResponse.WriteSubstitution method that accepts HttpResponseSubstitutionCallback delegate as parameter. The signature of the delegate is: string HttpResponseSubstitutionCallback(HttpContext). This method allows us to register insertion points with the response output. Before the output is send to the client, all registered callbacks are called and the returned strings are inserted into the output stream at the corresponding insertion points. Pretty neat, but it is not so straightforward when and how to do the substitution. The delegates are called way too early in the request / response lifecycle and therefore there is no session state, page, controls or anything as we know it the regular page request. In most cases, in the delegates you will just read some data and return a string with hardcoded HTML formatting and that makes perfect sense as all this is intended for maximum performance.


How Sitefinity extends cache substitution?

Sitefinity provides two base classes
CacheSubstitutionUserControl and
CacheSubstitutionCompositeControl


These controls automate the registration and rendering process. For the sake of simplicity, from now on I will talk about CacheSubstitutionUserControl only but everything applies to CacheSubstitutionCompositeConrtol as well.

The control provides three page modes that define the control’s behavior. We will see examples and examine what are the pros and cons for each of the modes. For that purpose I picked a very common scenario. Actually there will be two scenarios which are slightly different, that will demonstrate all possible ways to use these controls.


In the first scenario, we have a page that displays the user name of the user making the current request in the header of the page. If the current request is made by unauthenticated user, instead of the name we will display a link to the login page. The only difference in the second scenario will be that instead of link to the login page, we will display a login form directly in the header of the page.


Let’s use Telerik International University sample project for this demo. The first example will demonstrate the control using PageMode set to “None”. Using the control in this mode is almost identical as directly using HttpResponse.WriteSubstitution method except that it allows us to encapsulate the implementation in the control and use it on Sitefinity pages.


1. Create new folder in UserControls folder named CacheSubstitutionDemo.
2. Create new user control named UserInfo.ascx.
3. Open the code behind and change the class to inherit form CacheSubstitutionUserControl instead of UserControl.
4. Copy the methods form the sample code below to the body of your class: 

using System;  
using System.Web;  
using System.Web.Security;  
using Telerik.Cms.Web.UI;  
using Telerik.Web;  
 
public partial class UserControls_Login_UserInfo : CacheSubstitutionUserControl  
{  
    public override SubstitutionPageMode PageMode  
    {  
        get 
        {  
            return SubstitutionPageMode.None;  
        }  
    }  
 
    public override HttpResponseSubstitutionCallback SubstitutionCallback  
    {  
        get 
        {  
            return GetUserInfo;  
        }  
    }  
 
    public static string GetUserInfo(HttpContext context)  
    {  
        if (context.Request.IsAuthenticated)  
        {  
            return String.Format("<strong style=\"float:left;\">{0}</strong>", Membership.GetUser().UserName);  
        }  
        else 
        {  
            return String.Format("<a href=\"{0}\" style=\"float:left;\">Log in</a>", UrlPath.ResolveUrl("~/Sitefinity/Login.aspx"));  
        }  
    }  
}  
 

 
5. Register your control with Sitefinity’s Control Toolbox in web.config as shown below:

<toolboxControls> 
   <clear /> 
   <add name="User Info" section="Most popular" url="~/UserControls/CacheSubstitutionDemo/UserInfo.ascx" /> 
   ... 

and set default caching provider to ASPNET

<framework> 
      <caching defaultProvider="ASPNET">  
        <providers> 
          <add name="memoryCache" type="Telerik.Caching.MemoryCachingProvider, Telerik.Framework"/>  
          <add name="ASPNET" type="Telerik.Caching.AspNetCachingProvider, Telerik.Framework" duration="120" slidingExpiration="true"/>  
        </providers> 
        <cacheDependency mode="InMemory"/>  
      </caching> 
  ... 

6. Run the web site, navigate to ~/Sitefinity/Admin/Templates.aspx and open TIU3Column template.
7. Drop the control in MainNavigation place holder.
8. Save the changes
9. Got to Pages section, select Home page, go to Properties and turn on caching.
10. Save the changes and examine the home page on the public side.

 

Note that cache substitution works only with ASPNET caching provider.


In this example we are just exposing static HttpResponseSubstitutionCallback delegate in which we do the entire implementation. The base control registers the delegate with the response for us. Note that if you declare any controls in the .ascx file, they won’t be rendered. Our control will display only whatever our GetUserInfo method returns. This is because after the initial request, no instance of our control or page will be created. Only our static method will be invoked. Obviously this is very efficient method as the only overhead for the response will be whatever we do inside the static method. However this method will be very inconvenient if we have to do something more complex which will become particularly difficult to maintain later. That’s why CacheSubstitutionUserControl provides the two other modes.


In the second example we will see how Partial mode woks and how it defers form the previous.

1. In the CacheSubstitutionDemo folder create new user control named UserInfoPartial.ascx
2. Replace the declarations in the .ascx file with the sample below.

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="UserInfoPartial.ascx.cs" Inherits="UserControls_CacheSubstitutionDemo_UserInfoPartial" %> 
<asp:PlaceHolder ID="loginFormHolder" runat="server">  
    <asp:HyperLink ID="loginForm" NavigateUrl="~/Sitefinity/Login.aspx" Text="Log In" runat="server"></asp:HyperLink> 
</asp:PlaceHolder> 
<asp:PlaceHolder ID="userInfoHolder" runat="server">  
    <asp:Label ID="userInfo" runat="server"></asp:Label> 
</asp:PlaceHolder> 
 

3. Replace the code in the code behind file from the sample below.

using System.Web;  
using System.Web.Security;  
using System.Web.UI;  
using Telerik.Cms.Web.UI;  
 
public partial class UserControls_CacheSubstitutionDemo_UserInfoPartial : CacheSubstitutionUserControl  
{  
    protected override void Render(HtmlTextWriter writer)  
    {  
        bool isAuthenticated = HttpContext.Current.Request.IsAuthenticated;  
        this.userInfoHolder.Visible = isAuthenticated;  
        this.loginForm.Visible = !isAuthenticated;  
        if (isAuthenticated)  
            this.userInfo.Text = Membership.GetUser().UserName;  
 
        base.Render(writer);  
    }  
 
    public override SubstitutionPageMode PageMode  
    {  
        get 
        {  
            return SubstitutionPageMode.Partial;  
        }  
    }  
}  
 

4. Do the steps from 5 to 10 from the previous example.


As you have probably noticed, this control does exactly the same what the previous one does but allows us to easily maintain its layout. What happens behind the scenes is this: Sitefinity registers a callback just like the one in the first example. When the call back is invoked Sitefinity creates an instance of our control, renders it outside the current HttpContext and then returns the rendered HTML as result of the callback. This is little bit more work but still very efficient as only our controls is rendered. The drawback of this method is that the control is rendered outside of the context and therefore no events or control state is handled. That’s why we are overriding Render method to do our logic instead of the usual Page_Load event.


The third mode allows us to have fully functional control with events, control state and postbacks. Of course it adds a bit more to the overhead.


1. Crate new user control and name it LoginForm.ascx.
2. Replace the declarations in the .ascx file with the sample below.

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="LoginForm.ascx.cs" Inherits="UserControls_CacheSubstitutionDemo_LoginForm" %> 
<div id="LoginFormHolder" runat="server">  
    <asp:Literal ID="ErrorMessage" runat="server">  
        <strong style="color:Red;">Wrong user name or password!</strong>&nbsp;&nbsp;&nbsp;  
    </asp:Literal> 
    <asp:Label ID="UserNameLabel" Text="User Name" AssociatedControlID="UserName" runat="server"></asp:Label>&nbsp;  
    <asp:TextBox ID="UserName" runat="server"></asp:TextBox>&nbsp;&nbsp;&nbsp;  
    <asp:Label ID="PasswordLabel" Text="Password" AssociatedControlID="Password" runat="server"></asp:Label>&nbsp;  
    <asp:TextBox ID="Password" TextMode="Password" runat="server"></asp:TextBox>&nbsp;  
    <asp:Button ID="Login" Text="Log in" runat="server" OnClick="Login_Click" /> 
</div> 
<div id="UserInfoHolder" runat="server">  
    <span>Welcome&nbsp;  
    <strong> 
        <asp:Literal ID="UserInfo" runat="server"></asp:Literal> 
    </strong>&nbsp;  
    <asp:LinkButton ID="LogOut" Text="Log Out" OnClick="LogOut_Click" runat="server"></asp:LinkButton></span>  
</div> 
 

3. Replace the code in the code behind file from the sample below.

using System;  
using System.Web;  
using System.Web.Security;  
using System.Web.UI;  
using Telerik.Cms.Web.UI;  
 
public partial class UserControls_CacheSubstitutionDemo_LoginForm : CacheSubstitutionUserControl  
{  
    public override SubstitutionPageMode PageMode  
    {  
        get 
        {  
            return SubstitutionPageMode.Full;  
        }  
    }  
 
    protected void Page_Load(object sender, EventArgs e)  
    {  
        if (!this.IsPostBack)  
            this.SetControls(false);  
    }  
 
    protected void Login_Click(object sender, EventArgs e)  
    {  
        if (Membership.ValidateUser(this.UserName.Text, this.Password.Text))  
        {  
            FormsAuthentication.SetAuthCookie(this.UserName.Text, false);  
            this.Response.Redirect(this.Request.RawUrl, false);  
        }  
        else 
        {  
            this.SetControls(true);  
        }  
    }  
 
    protected void LogOut_Click(object sender, EventArgs e)  
    {  
        FormsAuthentication.SignOut();  
        this.Response.Redirect(this.Request.RawUrl, false);  
    }  
 
    private void SetControls(bool error)  
    {  
        bool isAuthenticated = HttpContext.Current.Request.IsAuthenticated;  
        this.UserInfoHolder.Style.Add(HtmlTextWriterStyle.Display, isAuthenticated ? "block" : "none");  
        this.LoginFormHolder.Style.Add(HtmlTextWriterStyle.Display, isAuthenticated ? "none" : "block");  
        this.ErrorMessage.Visible = error;  
        if (isAuthenticated)  
            this.UserInfo.Text = Membership.GetUser().UserName;  
    }  
 
    protected override void Render(HtmlTextWriter writer)  
    {  
        this.Page.ClientScript.RegisterForEventValidation(this.Login.ClientID);  
        this.Page.ClientScript.RegisterForEventValidation(this.LogOut.ClientID);  
        base.Render(writer);  
    }  
}  
 

4. Do the steps from 5 to 10 from the previous example.

9 comments

Leave a comment
  1. Shane Mar 17, 2009
    You note that cache substitution only works with the ASPNET caching provider. 

    How can I cache a whole page except predefined user controls (i.e shopping basket) with the database cache provider? You see we have a cluster of web servers and the database model would be far better for us!

    Secondly, I have a base usercontrol already being used for common functionality. Now I have an issue with having to implement this with two base classes, of course this isn't possible - why didn't you just provide a simple interface to use?

    Help please?

    Shane
  2. Vlad Mar 18, 2009
    Hi Shane,

    Yes, currently the cache substitution works only for the ASP.NET cache. We have plans to implement it for the other providers as well in one of the future releases. However, I am not sure you will get much benefit from using a database cache provider in a web farm. Maybe, as a whole, the performance will be the same as with the ASPNET caching provider.

    As for your second issues, it's not just an interface, there is a lot of logic behind, which probably will be changed and improved in the future. However, there is a workaround. You can make your common user control to inherit from CacheSubstitutionUserControl and override the SubstitutionPageMode property by setting it to None and override the Render method of the control:
    public override SubstitutionPageMode PageMode  
    {  
        get  
        {  
            return SubstitutionPageMode.None;  
        }  
    }  
     
    protected override void Render(System.Web.UI.HtmlTextWriter writer) 
        if (PageMode == SubstitutionPageMode.None && SubstitutionCallback == null
        { 
            this.RenderChildren(writer); 
        } 
        else 
        { 
            base.Render(writer); 
        } 


    Then, in your final substitution user control, you can just override this property and set it to SubstitutionPageMode.Partial or SubstitutionPageMode.Full
    public override SubstitutionPageMode PageMode 
        get 
        { 
            return SubstitutionPageMode.Full; 
        } 

    or override SubstitutionCallback property.

    In v3.6 SP1, there will be an option to completely disable the substitution behavior without overriding the Render method.

    Hope this is helpful.



  3. Duncan Mar 20, 2009
    You mentioned this:

    The delegates are called way too early in the request / response lifecycle and therefore there is no session state, page, controls or anything as we know it the regular page request.

    This is the issue i am having. This is a great thing to have for cached pages and still allowing portions of the page to be dynamic, but really how dynamic can the control get without having access to HttpContext.Current.Session, i mean som of the classes that my dynamic control calls requires the users SessionID for example. And if the page is cached and CacheSubstitutionUserControl is being used then HttpContext.Current.Session is null...

    I have been searching for days to resolve this very issue and found only one small example of providing HttpContext with Cache Output Subsitiution:

    http://www.vikramlakhotia.com/Substituting_part_of_the_page_using_substitution_control.aspx

    Is there something i am missing here to be able to access things like query string, Session variable, authentication detail etc, like the above example does?

    Thanks

    Duncan
  4. Bob Mar 23, 2009
    You are not missing anything. Session state is not available, but authentication information is available as the request is authenticated prior to output cache resolution. In fact the samples above work exclusively with authentication information. Query string is also available.

    In a future version we may provide the option to force loading session state as well, but for the time being you have to work around it.

    There are several possible ways to avoid using session state. You can use database to store temporary user info, you can retrieve the session id form the cookies and use it as key to store user info in the Cache instead of the Session object, you could create your own session handling or use query string instead.

    If you use the cache object to store session data, don’t forget to set appropriate expiration.
  5. ROMI Apr 15, 2009

    Hi Vlad,

    I have many problems with caching inside Admin side. Sessions, Cookies, QueryString and Databaseacces are not availables. I have tried many methods of Substitution with no issue. I have tried with simple drill dropdown in one View and passing some cookie or Session or QueryString nothing pass!. I have implemented as Usercontrols with cache substitution Full or Partial with no issue unable to refresh. It's not possible to update my contents in Admin side of sf. I have used other methods like Ajax prototype Update and Request. I have controled that generated uniques post and see the result in the stream updated but screen in no updated! Actually i'm bloqued in very important project. It's not nice! Romi

  6. Faraz Sep 16, 2009

    Hi Vlad,

    I have a header control in which I have sign in option. Once the user logs in his name appears in literal control placed in header. My problem is that since Sitefinity is caching the control it is displaying the name in the literal even to the people who are not logged in. Could you please advise how am I suppose to tell sitefinity not to cache specifically that litera/label control.

    Thanks
    Faraz.

  7. Faraz Nov 25, 2009
    Hi Vlad,

    The above method keep on getting the updated text, how am I suppose to update Text of some other WebControl.

    Thanks
    Faraz
  8. erter May 30, 2011
    rtftgf
  9. Patrick Jun 14, 2013
    I don't see the Telerik.Cms.Web.UI namespace anywhere. Is this not supported in Sitefinity 6? What is the workaround for this concept in Sitefinity 6? Thanks

    Leave a comment