Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC "Donut caching" and TempData

Tags:

Is there any solution for accessing TempData property in the HttpResponseBase.WriteSubstitution() method

This does not work:

<%= Response.WriteSubstitution(x => Html.Encode(TempData["message"].ToString())) %>

But this works:

<%= Response.WriteSubstitution(x => DateTime.Now.ToString()) %>

The problem is in request processing for once cached pages. According to http://msdn.microsoft.com/en-us/library/system.web.httpresponse.writesubstitution.aspx:

On the first request to the page, the WriteSubstitution calls the HttpResponseSubstitutionCallback delegate to produce the output. Then, it adds a substitution buffer to the response, which retains the delegate to call on future requests. Finally, it degrades client-side cacheability from public to server-only, ensuring future requests to the page re-invoke the delegate by not caching on the client.

In other words the delegate does not have access to Session property (SessionStateTempDataProvider stores TempData in session) because there is no "normal" request life cycle. As I understand it is processed at HttpApplication.ResolveRequestCache/HttpApplication.PostResolveRequestCache event, but the current state is acquired at the HttpApplication.AcquireRequestState event (http://msdn.microsoft.com/en-us/library/ms178473.aspx)

Maybe I need some kind of "advanced custom TempDataProvider" :) Any ideas?

like image 353
eu-ge-ne Avatar asked Mar 25 '09 21:03

eu-ge-ne


1 Answers

I've found the solution:

The main idea is in saving copy of TempData in the Cache and retreiving it on every request. The solution is a combination of custom TempDataProvider and simple http module. Plus there are couple of helpers and static classes.

Here is the code:

CustomTempDataProvider:

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Caching;
using System.Web.Mvc;

public class CustomTempDataProvider : SessionStateTempDataProvider, IHttpModule
{
    public void Init(HttpApplication application)
    {
        application.BeginRequest += new EventHandler(application_BeginRequest);
    }

    void application_BeginRequest(object sender, EventArgs e)
    {
        var httpContext = HttpContext.Current;
        var tempData = httpContext.Cache[TempDataKey] ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
        httpContext.Items.Add("TempData", tempData);
        httpContext.Cache.Remove(TempDataKey);
    }

    public override void SaveTempData(ControllerContext controllerContext,
        IDictionary<string, object> values)
    {
        HttpContext.Current.Cache.Insert(TempDataKey, values, null, DateTime.Now.AddMinutes(5), Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, null);
        base.SaveTempData(controllerContext, values);
    }

    public static string TempDataKey
    {
        get
        {
            string sessionID = "0";
            var httpContext = HttpContext.Current;
            if(httpContext.Session != null)
            {
                sessionID = httpContext.Session.SessionID;
            }
            else if (httpContext.Request.Cookies["ASP.NET_SessionId"] != null)
            {
                sessionID = httpContext.Request.Cookies["ASP.NET_SessionId"].Value;
            }
            return "TempData-For-Session-" + sessionID;
        }
    }

    public void Dispose()
    {
    }
}

Register it in the web.config:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.web>
        <httpModules>
            <add name="CustomTempDataProvider" type="CustomTempDataProvider" />
        </httpModules>
    </system.web>
    <system.webServer>
        <modules>
            <remove name="CustomTempDataProvider" />
            <add name="CustomTempDataProvider" type="CustomTempDataProvider" />
        </modules>
    </system.webServer>
</configuration>

CustomControllerFactory:

using System.Web.Routing;
using System.Web.Mvc;

public class CustomControllerFactory : DefaultControllerFactory
{
    public override IController CreateController(
        RequestContext requestContext, string controllerName)
    {
        var controller = (Controller)base.CreateController(requestContext, controllerName);
        controller.TempDataProvider = new CustomTempDataProvider();
        return controller;
    }
}

register it in the Global.asax:

protected void Application_Start()
{
    RegisterRoutes(RouteTable.Routes);

    ControllerBuilder.Current.SetControllerFactory(typeof(CustomControllerFactory));
}

Static class for accessing TempData:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

public static class CustomTempData
{
    public static object Get(string key)
    {
        var tempData = HttpContext.Current.Items["TempData"] as IDictionary<string, object>;
        var item = tempData.FirstOrDefault(x => x.Key == key).Value ?? String.Empty;
        return item;
    }
}

Helper for Post-cache Substitution:

using System;
using System.Web;
using System.Web.Mvc;

public static class Html
{
    public delegate object MvcResponseSubstitutionCallback(HttpContextBase context);

    public static object MvcResponseSubstitute(this HtmlHelper html, MvcResponseSubstitutionCallback callback)
    {
        html.ViewContext.HttpContext.Response.WriteSubstitution(
            context =>
                HttpUtility.HtmlEncode(
                    (callback(new HttpContextWrapper(context)) ?? String.Empty).ToString()
                )
            );

        return null;
    }
}

Now this works successfully:

<h3><%= Html.MvcResponseSubstitute(context => CustomTempData.Get("message")) %></h3>

If you understand russian, read this

Hope this helps

like image 83
eu-ge-ne Avatar answered Oct 21 '22 15:10

eu-ge-ne