Post-Cache Substitution

This post explains a new caching feature introduced in ASP.NET Whidbey that can be used by controls needing to display dynamic and different content on each request, even if the containing page is being output-cached. While illustrating this mechanism, I have also provided a helper class that creates the equivalent of the already established Render semantics within a Control to simplify usage of the underlying infrastructure.

This is a new ASP.NET Whidbey feature that has not received any significant advertisement as far as I know. This feature nicely rounds out the caching support offered by the framework. Previously you could output cache whole pages, or fragments of a page. However, you could not implement a dynamic region within a cached page. The best way to think of this scenario is the AdRotator control, which does make use of this new infrastructure. The entire page might be output cached, but a different ad still needs to be displayed on each request to the page. Post-cache substitution enables this with very little logic on your end. Rather than build the functionality into AdRotator, we provided it as a generic service. As an example, using this service, I could output cache my site's home page, and continue to change the random photo shown in the sidebar on each request.

The idea is basically this - as a control developer, you implement your dynamic rendering functionality in a callback, and register that callback with the output Response. The cached response then effectively contains a marker, which is substituted with your real rendering before the output is streamed down to the client. When a subsequent request comes in, the cached rendering is retrieved, your callback is invoked again, the substitutions are made again, and the result is sent down. Thus the work of rendering the entire page can be skipped, and only the minimal work needed to update the dynamic region needs to be done.

The basic API that enables post-cache substitution is the following method on HttpResponse:

void WriteSubstitution(HttpResponseSubstitutionCallback callback)

The callback that you implement is defined as a method that takes in an HttpContext instance, and returns a string. The returned string is used to substitute the marker placed in the cached content.

It is too late to add this to Whidbey, but I wish we had a slightly more structured (or if I could I coin a new term: "frameworky") mechanism for control developers. To that end, I have put together a ResponseSubstitution class you can derive from. The public OM on this class is defined as follows:

public abstract class ResponseSubstitution {
    protected HttpContext Context { get; }
    public void Render(HttpContext context, HtmlTextWriter writer);
    protected abstract void Render(HtmlTextWriter writer);
}

The class encapsulates the substitution callback, the call to WriteSubstitution, and the logic to create an instance of the same HtmlTextWriter being used to render the page during the first render. In your derived class you simply override Render and write using the supplied HtmlTextWriter, which is more consistent with the virtual Render method from Control rather than having to implement a delegate which takes in an HttpContext and returns a string. Feel free to copy the code for this class (included at the end of this post), and use it for your own projects...

Here is a very simple control that uses it - a RandomNumberLabel.

public class RandomNumberLabel : Label {

    public int MaximumValue { get; set; }
    public int MinimumValue { get; set; }

    protected override void Render(HtmlTextWriter writer) {
        base.RenderBeginTag(writer);

        RandomNumberLabelResponseSubstitution substitution =
            new RandomNumberLabelResponseSubstitution(MinimumValue, MaximumValue);
        substitution.Render(Context, writer);

        base.RenderEndTag();
    }

    private class RandomNumberLabelResponseSubstitution : ResponseSubstitution {

        private static readonly Random _random = new Random();
        private int _minValue;
        private int _maxValue;

        public RandomNumberLabelResponseSubstitution(int minValue, int maxValue) {
            _minValue = minValue;
            _maxValue = maxValue;
        }

        protected override void Render(HtmlTextWriter writer) {
            int randomValue = _random.Next(_maxValue) + _minValue;
            writer.Write(randomValue.ToString());
        }
    }
}

This shows just how simple it is to implement post-cache substitution. More realistic examples of controls requiring this functionality include RandomPhoto, QuoteOfDay, and ContentRotator. However, even these would pretty much follow the same pattern using my ResponseSubstitution class.

I wanted to call out a couple of things from the sample code above:

  1. I transferred the required context to the substitution class, so I don't need to keep a reference to the control alive. More on why this is important in a bit.
  2. I rendered the minimal content in the substitution. For example, I rendered the tags around the content in the control itself. This reduces the amount of context that needs to be transferred to the substitution class.

And here is a snippet from the sample page:

<%@ OutputCache Duration="60" VaryByParam="*" />
<script runat="server">
void Page_Load() {
	timeStampLabel.Text = DateTime.Now.ToString();
}
</script>
<asp:Label runat="server" id="timeStampLabel" />
<br />
Random Number: <nk:RandomNumberLabel runat="server" ForeColor="Red" />

You'll see the random number change on each request, but the time stamp only updates when the page's cache entry expires after 60 seconds, and the complete page is re-rendered.

A few of things to be aware of when using post-cache substitution:

  • This dynamically disables public (or client) caching and switches the page to use server caching. This is because the substitutions need to happen via server-side logic.
  • The page does not get the performance benefits of kernel mode caching if it has controls using this mechanism. But this is also expected, since some work needs to be done to process the request.
  • Your control can use this mechanism even if the page is not being cached. In that case, your callback would be called once (and immediately). This allows you to write code in one way, regardless of whether the page developer has turned on caching or not.

And finally one rule to follow:
It might be very tempting to hold a reference to the control in your substitution class. You should avoid this. Period. Factor your code, so you don't have to. For that matter, you might be wondering why I even had a substitution class, and didn't implement the callback within the control class itself. This is because if the substitution code held on to the control, it ends up also holding on the page containing the control, and every control within that page. Basically that page instance then cannot be garbage collected until the cache entry itself expires and the underlying infrastructure lets go of the callback delegate. This is not desirable.

I am curious: What features do you think will benefit from this capability?

Here is the code to ResponseSubstitution. Enjoy!

// ResponseSubstitution.cs
// Copyright (c) Nikhil Kothari, 2005.
//

using System;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace NikhilK.Samples {

    /// <devdoc>
    ///   Provides basic Control rendering pattern on top of Post-Cache Substitution
    ///   infrastructure.
    /// </devdoc>
    public abstract class ResponseSubstitution {

        private ConstructorInfo _writerConstructor;
        private HttpContext _context;

        protected ResponseSubstitution() {
        }

        protected HttpContext Context {
            get {
                return _context;
            }
        }

        public void Render(HttpContext context, HtmlTextWriter writer) {
            if (context == null) {
                throw new ArgumentNullException("context");
            }
            if (writer == null) {
                throw new ArgumentNullException("writer");
            }

            Type writerType = writer.GetType();
            Type[] constructorArgs = new Type[] { typeof(TextWriter) };

            _writerConstructor = writer.GetType().GetConstructor(constructorArgs);
            if (_writerConstructor == null) {
                throw new InvalidOperationException("The HtmlTextWriter does not have a public constructor taking in a TextWriter");
            }

            HttpResponseSubstitutionCallback callback =
                new HttpResponseSubstitutionCallback(this.RenderCallback);
            context.Response.WriteSubstitution(callback);
        }

        protected abstract void Render(HtmlTextWriter writer);

        private string RenderCallback(HttpContext context) {
            StringWriter baseWriter = new StringWriter(CultureInfo.CurrentCulture);
            HtmlTextWriter writer = (HtmlTextWriter)_writerConstructor.Invoke(new object[] { baseWriter });

            try {
                _context = context;
                Render(writer);
            }
            finally {
                _context = null;
            }
            return baseWriter.ToString();
        }
    }
}

Updated (1/23/2005): Added the missing cast to fix code as per comment below.

Posted on Saturday, 1/22/2005 @ 10:36 AM | #ASP.NET


Comments

8 comments have been posted.

Erwyn van der Meer

Posted on 1/22/2005 @ 2:47 PM
This looks like a cool idea. Too bad it's to late to add your ResponseSubstition class to ASP.NET 2.0. I spotted one problem in your code. You are missing a cast in the line

HtmlTextWriter writer = _writerConstructor.Invoke(new object[] { baseWriter });

It should be

HtmlTextWriter writer = (HtmlTextWriter) _writerConstructor.Invoke(new object[] { baseWriter });

David Taylor

Posted on 1/22/2005 @ 7:00 PM
>For that matter, you might be wondering why I even had a substitution class, and didn't implement the callback within the control class itself. This is because if the substitution code held on to the control....

But surely that GC sutuation is only true if your delegate is referencing an instance method and not a static method...
....see below...

HttpResponseSubstitutionCallback callback =
new HttpResponseSubstitutionCallback(MyClass.RenderCallback);
context.Response.WriteSubstitution(callback);
}
.......
private STATIC string RenderCallback(HttpContext context) {
.......

I would have thought it the delegate is referencing a static method, the GC would be able to collect the instance?

David

Nikhil Kothari

Posted on 1/23/2005 @ 9:19 AM
Erwyn, good catch. The code is now fixed... I made a last minute change outside VS to improve the reflection code. :-)

David,
I can imagine the instance can be GC'd when you have a delegate to a static method. However, this static method would need some contextual data from the control in order to do its work (In the example, it needs the min and max values). If the delegate was indeed designed for being implemented as a static method the APIs would have been:
public void WriteSubstitution(HttpResponseSubstitutionCallback callback, object data)
public delegate string RenderCallback(HttpContext context, object data);

However, this is not the case. The delegate designed to be implemented as an instance method, where the class instance contains the required contextual information. Hence I defined the ResponseSubstitution class. Without this in place, it would be natural for someone to remove the "static" off the delegate at some point even though it is on the control implementation, leading to the problems I mention.

Bertrand

Posted on 1/24/2005 @ 12:13 PM
On a side note, professional web ad services use all kinds of tricks to avoid being cached by their host sites and by the clients. Usually, it's a mix of javascript and iframes, but no server code is possible for these guys.

Darren Neimke

Posted on 2/18/2005 @ 5:49 PM
I've posted a working demo of this function (based on the DEC CTP) here:

http://www.projectdistributor.net/Projects/Project.aspx?projectId=75

Rick Strahl

Posted on 7/16/2005 @ 3:25 AM
This feature is pretty limited as it is right now. The HttpContext instance passed doesn't include Session State, which means you have no way to do anything that is related to the current user - you can only do things that are 'random' which is what all your examples list. We really need some option to get state passed along, even if it means more overhead.

Common scenario is a homepage that's all static except for a the user's statistics. Or a store with a running shopping cart summary for the user etc. etc.

Maestrocity

Posted on 12/2/2005 @ 9:44 PM
I wouldn't say that this "rounds out" anything, when ASP.NET still ignores the single most basic caching mechanism of the web: etags. But kudos for trying.

Nikhil Kothari

Posted on 12/6/2005 @ 5:26 AM
ASP.NET does support etags, and uses it. It also allows custom handlers to make use of it. In fact, the rss feed on my site uses etags...

Etags solve different problems - bandwidth usage. Post-cache substitution is designed to save cpu usage and response time on the server. Same with output caching...
The discussion on this post has been closed. Please use my contact form to provide comments.