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:
- 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.
- 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.