Logo

Maarten Balliauw {blog}

ASP.NET, ASP.NET MVC, Azure, PHP, OpenXML, VSTS, ...

About the author

Maarten Balliauw is an MVP ASP.NET and is currently employed as .NET Software Engineer at RealDolmen. His interests are mainly web applications developed in ASP.NET (C#) or PHP.
More about me More about me
Send mail E-mail me


Microsoft Most Valuable Professional - MVP - ASP.NET

Subscribe to my RSS feed Follow me on Twitter! View Maarten Balliauw's profile on LinkedIn RealDolmen - Rock-solid passion for ICT
I'm a speaker at TechDays Belgium and TechDays Finland

Search

Latest Twitter

    Follow me on Twitter...

    Disclaimer

    The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

    © Copyright Maarten Balliauw 2010

    Extending ASP.NET MVC OutputCache ActionFilterAttribute - Adding substitution

    In my previous blog post on ASP.NET MVC OutputCache, not all aspects of "classic" ASP.NET output caching were covered. For instance, substitution of cached pages. Allow me to explain...

    When using output caching you might want to have everything cached, except, for example, a user's login name or a time stamp. When caching a full HTTP response, it is not really possible to inject dynamic data. ASP.NET introduced the Substitution control, which allows parts of a cached response to be dynamic. The contents of the Substitution control are dynamically injected after retrieving cached data, by calling a certain static method which returns string data. Now let's build this into my OutputCache ActionFilterAttribute...

    UPDATE: Also check Phil Haack's approach to this: http://haacked.com/archive/2008/11/05/donut-caching-in-asp.net-mvc.aspx

    1. But... how?

    Schematically, the substitution process would look like this:

    ASP.NET MVC OutputCache

    When te view is rendered, it outputs a special substitution tag (well, special... just a HTML comment which will be recognized by the OutputCache). The OutputCache will look for these substitution tags and call the relevant methods to provide contents. A substitution tag will look like <!--SUBSTITUTION:CLASSNAME:METHODNAME-->.

    One side note: this will only work with server-side caching (duh!). Client-side could also be realized, but that would involve some Ajax calls.

    2. Creating a HtmlHelper extension method

    Every developer loves easy-to-use syntax, so instead of writing an error-prone HTML comment like <!--SUBSTITUTION:CLASSNAME:METHODNAME-->. myself, let's do that using an extension method which allows syntax like <%=Html.Substitution<MvcCaching.Views.Home.Index>("SubstituteDate")%>. Here's an example view:

    <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
        AutoEventWireup="true" CodeBehind="Index.aspx.cs"
        Inherits="MvcCaching.Views.Home.Index" %>
    <%@ Import Namespace="MaartenBalliauw.Mvc.Extensions" %>

    <asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
        <h2><%= Html.Encode(ViewData["Message"]) %></h2>

        <p>
            Cached timestamp: <%=Html.Encode(DateTime.Now.ToString())%>
        </p>

        <p>
            Uncached timestamp (substitution):
            <%=Html.Substitution<MvcCaching.Views.Home.Index>("SubstituteDate")%>
        </p>
    </asp:Content>

    The extension method for this will look quite easy. Create a new static class containing this static method:

    public static class CacheExtensions
    {
        public static string Substitution<T>(this HtmlHelper helper, string method)
        {
            // Check input
            if (typeof(T).GetMethod(method, BindingFlags.Static | BindingFlags.Public) == null)
            {
                throw new ArgumentException(
                    string.Format("Type {0} does not implement a static method named {1}.",
                        typeof(T).FullName, method),
                            "method");
            }

            // Write output
            StringBuilder sb = new StringBuilder();

            sb.Append("<!--");
            sb.Append("SUBSTITUTION:");
            sb.Append(typeof(T).FullName);
            sb.Append(":");
            sb.Append(method);
            sb.Append("-->");

            return sb.ToString();
        }
    }

    What happens is basically checking for the existance of the specified class and method, and rendering the appropriate HTML comment. Our example above will output <!--SUBSTITUTION:MvcCaching.Views.Home.Index:SubstituteDate-->.

    One thing to do before substituting data though: defining the SubstituteDate method on the MvcCaching.Views.Home.Index: view codebehind. The signature of this method should be static, returning a string and accepting a ControllerContext parameter. In developer language: static string MyMethod(ControllerContext context);

    Here's an example:

    public partial class Index : ViewPage
    {
        public static string SubstituteDate(ControllerContext context)
        {
            return DateTime.Now.ToString();
        }
    }

    3. Extending the OutputCache ActionFilterAttribute

    Previously, we did server-side output caching by implementing 2 overrides of the ActionFilterAttribute, namely OnResultExecuting and OnResultExecuted. To provide substitution support, we'll have to modify these 2 overloads a little. Basically, just pass all output through the ResolveSubstitutions method. Here's the updated OnResultExecuted overload:

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        // Server-side caching?
        if (CachePolicy == CachePolicy.Server || CachePolicy == CachePolicy.ClientAndServer)
        {
            if (!cacheHit)
            {
                // Fetch output
                string output = writer.ToString();

                // Restore the old context
                System.Web.HttpContext.Current = existingContext;

                // Fix substitutions
                output = ResolveSubstitutions(filterContext, output);

                // Return rendered data
                existingContext.Response.Write(output);

                // Add data to cache
                cache.Add(
                    GenerateKey(filterContext),
                    writer.ToString(),
                    null,
                    DateTime.Now.AddSeconds(Duration),
                    Cache.NoSlidingExpiration,
                    CacheItemPriority.Normal,
                    null);
            }
        }
    }

    Now how about this ResolveSubstitutions method? This method is passed the ControllerContext and the unmodified HTML output. If no substitution tags are found, it returns immediately. Otherwise, a regular expression is fired off which will perform replaces depending on the contents of this substitution variable.

    One thing to note here is that this is actually a nice security hole! Be sure to ALWAYS Html.Encode() dynamic data, as users can inject these substitution tags easily in your dynamic pages and possibly receive useful error messages with context information...

    private string ResolveSubstitutions(ControllerContext filterContext, string source)
    {
        // Any substitutions?
        if (source.IndexOf("<!--SUBSTITUTION:") == -1)
        {
            return source;
        }

        // Setup regular expressions engine
        MatchEvaluator replaceCallback = new MatchEvaluator(
            matchToHandle =>
            {
                // Replacements
                string tag = matchToHandle.Value;

                // Parts
                string[] parts = tag.Split(':');
                string className = parts[1];
                string methodName = parts[2].Replace("-->", "");

                // Execute method
                Type targetType = Type.GetType(className);
                MethodInfo targetMethod = targetType.GetMethod(methodName, BindingFlags.Static | BindingFlags.Public);
                return (string)targetMethod.Invoke(null, new object[] { filterContext });
            }
        );
        Regex templatePattern = new Regex(@"<!--SUBSTITUTION:[A-Za-z_\.]+:[A-Za-z_\.]+-->", RegexOptions.Multiline);

        // Fire up replacement engine!
        return templatePattern.Replace(source, replaceCallback);
    }

    How easy was all that? You can download the full soure and an example here.

    kick it on DotNetKicks.com


    Categories: ASP.NET | C# | General | Internet | MVC

    Comments

    alvinashcraft.com | Reply

    Tuesday, July 01, 2008 2:27 PM

    pingback

    Pingback from alvinashcraft.com

    Dew Drop - July 1, 2008 | Alvin Ashcraft's Morning Dew

    Troy Goode United States | Reply

    Tuesday, July 01, 2008 3:25 PM

    Troy Goode

    Very impressive Maarten! This is just what I'm looking for on one of my projects.

    dotnetwitter.wordpress.com | Reply

    Wednesday, July 09, 2008 12:44 PM

    pingback

    Pingback from dotnetwitter.wordpress.com

    links for 2008-07-09 « Praveen’s Blog

    Steve Sanderson United Kingdom | Reply

    Thursday, July 31, 2008 6:45 PM

    Steve Sanderson

    Maarten, this is really good, and in some ways more useful than the official built-in [OutputCache] filter introduced in Preview 4.

    However, I think there's a slight mistake! When you use OnRequestExecuting(), you should really be using OnActionExecuting() - otherwise you're running the action method every time, even when it's "cached". I tried you code, changing OnRequestExecuting() to OnActionExecuting(), and it works really well.

    Nice one.

    Haacked United States | Reply

    Friday, October 17, 2008 6:33 PM

    Haacked

    Slick approach. One thing to note is that if you use the WebFormViewEngine you can use the declarative substitution control. Yes, that would require you to put some code in the code-behind (egads!) and that code would not have access to the ControllerContext, but for really simple donuts where the info you need is all accessible by the HttpContext, that would work.

    you've been HAACKED | Reply

    Thursday, November 06, 2008 1:45 AM

    trackback

    Trackback from you've been HAACKED

    Donut Caching in ASP.NET MVC

    Frugal Coder Blog | Reply

    Tuesday, November 11, 2008 10:59 PM

    trackback

    Trackback from Frugal Coder Blog

    MySite - Part 1

    梦想永存 | Reply

    Saturday, May 30, 2009 10:49 AM

    trackback

    asp.net mvc Partial OutputCache 在SpaceBuilder中的应用实践

    目前SpaceBuilder表现层使用是asp.net mvc v1.0,使用了很多RenderAction(关于asp.net mvc的Partial Requests参见Partial Requests in ASP.NET MVC)。希望对于实时性要求不高的内容区域采用客户端缓存来提升性能同时也弥补一下RenderAction对性能的损失。

    使用asp.net mvc自带的OutputCache Filter时发现了一个可怕的bug,在View中任何一个RenderAction设置OutputCache却影响了整个View。搜索发现确实是asp.net mvc目前已知的一个bug ,关于该问题的解决也有很多人提出了自己的方法。

    Add comment




      Country flag

    biuquote
    • Comment
    • Preview
    Loading