Skip to content

Cookie based TempData provider

June 11, 2012

TempData is a nice feature in MVC and, if I am not mistaken, was inspired by the Flash module from Ruby on Rails. It’s basically a way to maintain some state across a redirect.

In Rails the default implementation uses a cookie to store the data which makes this a fairly lightweight mechanism for passing data from one request to the next. Unfortunately the default implementation of TempData in MVC uses Session State as the backing store which makes it less than ideal. That’s why I wanted to show how to build an implementation that uses a cookie, so here’s the CookieTempDataProvider.

In implementing this it was important to acknowledge that the data stored in TempData is being issued as a cookie to the client, which means it’s open to viewing and modification by the end user. As such, I wanted to add protections from modification and viewing (modification being the more important of the two). The implementation uses the same protection facilities as the ASP.NET machine key mechanisms for encrypting and signing Forms authentication cookies and ViewState. We’re also doing compression since cookies are limited in size.

The code is available on GitHub. I also wrapped this up into a NuGet package (BrockAllen.CookieTempData) so all that’s necessary is to reference the assembly via NuGet and all controllers will now use the Cookie-based TempData provider. If you’re interested in mode details, read on…

The code is fairly self-explanatory:

public class CookieTempDataProvider : ITempDataProvider
{
    const string CookieName = "TempData";

    public void SaveTempData(
        ControllerContext controllerContext,
        IDictionary<string, object> values)
    {
        // convert the temp data dictionary into json
        string value = Serialize(values);
        // compress the json (it really helps)
        var bytes = Compress(value);
        // sign and encrypt the data via the asp.net machine key
        value = Protect(bytes);
        // issue the cookie
        IssueCookie(controllerContext, value);
    }

    public IDictionary<string, object> LoadTempData(
        ControllerContext controllerContext)
    {
        // get the cookie
        var value = GetCookieValue(controllerContext);
        // verify and decrypt the value via the asp.net machine key
        var bytes = Unprotect(value);
        // decompress to json
        value = Decompress(bytes);
        // convert the json back to a dictionary
        return Deserialize(value);
    }
    string GetCookieValue(ControllerContext controllerContext)
    {
        HttpCookie c = controllerContext.HttpContext.Request.Cookies[CookieName];
        if (c != null)
        {
            return c.Value;
        }
        return null;
    }

    void IssueCookie(ControllerContext controllerContext, string value)
    {
        HttpCookie c = new HttpCookie(CookieName, value)
        {
            // don't allow javascript access to the cookie
            HttpOnly = true,
            // set the path so other apps on the same server don't see the cookie
            Path = controllerContext.HttpContext.Request.ApplicationPath,
            // ideally we're always going over SSL, but be flexible for non-SSL apps
            Secure = controllerContext.HttpContext.Request.IsSecureConnection
        };

        if (value == null)
        {
            // if we have no data then issue an expired cookie to clear the cookie
            c.Expires = DateTime.Now.AddMonths(-1);
        }

        if (value != null || controllerContext.HttpContext.Request.Cookies[CookieName] != null)
        {
            // if we have data, then issue the cookie
            // also, if the request has a cookie then we need to issue the cookie
            // which might act as a means to clear the cookie 
            controllerContext.HttpContext.Response.Cookies.Add(c);
        }
    }

    string Protect(byte[] data)
    {
        if (data == null || data.Length == 0) return null;

        return MachineKey.Encode(data, MachineKeyProtection.All);
    }

    byte[] Unprotect(string value)
    {
        if (String.IsNullOrWhiteSpace(value)) return null;

        return MachineKey.Decode(value, MachineKeyProtection.All);
    }

    byte[] Compress(string value)
    {
        if (value == null) return null;

        var data = Encoding.UTF8.GetBytes(value);
        using (var input = new MemoryStream(data))
        {
            using (var output = new MemoryStream())
            {
                using (Stream cs = new DeflateStream(output, CompressionMode.Compress))
                {
                    input.CopyTo(cs);
                }

                return output.ToArray();
            }
        }
    }

    string Decompress(byte[] data)
    {
        if (data == null || data.Length == 0) return null;

        using (var input = new MemoryStream(data))
        {
            using (var output = new MemoryStream())
            {
                using (Stream cs = new DeflateStream(input, CompressionMode.Decompress))
                {
                    cs.CopyTo(output);
                }

                var result = output.ToArray();
                return Encoding.UTF8.GetString(result);
            }
        }
    }

    string Serialize(IDictionary<string, object> data)
    {
        if (data == null || data.Keys.Count == 0) return null;

        JavaScriptSerializer ser = new JavaScriptSerializer();
        return ser.Serialize(data);
    }

    IDictionary<string, object> Deserialize(string data)
    {
        if (String.IsNullOrWhiteSpace(data)) return null;

        JavaScriptSerializer ser = new JavaScriptSerializer();
        return ser.Deserialize<IDictionary<string, object>>(data);
    }
}

Normally to use a custom TempDataProvider you must override CreateTempDataProvider from the Controller base class as such:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    protected override ITempDataProvider CreateTempDataProvider()
    {
        return new CookieTempDataProvider();
    }
}

Ick — this means you either have to override this in each controller or have to have planned ahead and created a common base Controller class for the entire application. Fortunately there’s another way — the TempDataProvider is assignable on the Controller base class. This means that upon controller creation you can assign it and this can easily be done in a custom controller factory which I implemented in the NuGet package:

class CookieTempDataControllerFactory : IControllerFactory
{
    IControllerFactory _inner;
    public CookieTempDataControllerFactory(IControllerFactory inner)
    {
        _inner = inner;
    }

    public IController CreateController(RequestContext requestContext, string controllerName)
    {
        // pass-thru to the normal factory
        var controllerInterface = _inner.CreateController(requestContext, controllerName);
        var controller = controllerInterface as Controller;
        if (controller != null)
        {
            // if we get a MVC controller then add the cookie-based tempdata provider
            controller.TempDataProvider = new CookieTempDataProvider();
        }
        return controller;
    }

    public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
    {
        return _inner.GetControllerSessionBehavior(requestContext, controllerName);
    }

    public void ReleaseController(IController controller)
    {
        _inner.ReleaseController(controller);
    }
}

The last thing is to configure the custom controller factory, and from a separate assembly this was done via the magic of PreApplicationStartMethod which allows for code to run prior to Applicaton_Start:

[assembly: PreApplicationStartMethod(typeof(BrockAllen.CookieTempData.AppStart), "Start")]

namespace BrockAllen.CookieTempData
{
    public class AppStart
    {
        public static void Start()
        {
            var currentFactory = ControllerBuilder.Current.GetControllerFactory();
            ControllerBuilder.Current.SetControllerFactory(new CookieTempDataControllerFactory(currentFactory));
        }
    }
}

Anyway, that’s it. Feedback is always welcome.

38 Comments leave one →
  1. November 16, 2012 9:15 am

    Note that the MachineKey.Encode method has security issues and should be avoided where possible, especially on .Net 4.5.

    http://blogs.msdn.com/b/webdev/archive/2012/10/23/cryptographic-improvements-in-asp-net-4-5-pt-2.aspx

    • December 21, 2012 4:02 am

      Is MachineKey.Encode safe to use on .NET 4.0?

      • December 21, 2012 10:14 am

        @Daniel15, it’s what ASP.NET sits on, so it’s the best game in town (for 4.0).

  2. manight permalink
    December 17, 2012 6:36 am

    You are still using MachineKey.Encode here Brock instead of the new MachineKey.Protect. Am I missing something?

  3. Daniel permalink
    December 21, 2012 4:03 am

    Is MachineKey.Encode safe to use on .NET 4.0?

  4. January 9, 2013 8:03 pm

    beautiful solution!

  5. alireza permalink
    February 4, 2013 11:59 am

    i have mvc 4 and .Net 4.5,
    I installed cookietempdata via nuget but that don’t work and get error,
    How must enable cookietempdata which instaled via nuget?

  6. alireza permalink
    February 4, 2013 4:11 pm

    I Installed CookieTempData via Nuget,
    How i must config/Enable CookieTempData in My Project(MVC 4 and .Net 4.5)?

    • February 4, 2013 4:19 pm

      It registers itself automatically. If you’re getting an error, submit it to the issues page I linked above.

      • alireza permalink
        February 4, 2013 4:32 pm

        I use structuremap and also i disabled session in web.config, CookieTempData have problem with these issues?

      • Kenneth Fiduk permalink
        September 4, 2013 3:55 pm

        What if we want it to be configurable? The package is fantastic and works great, but if another dev picked up my work, they would have NO idea that I’m using a custom TempDataProvider by looking at my project. They’d have to know that the mere presence of the BrockAllen.CookieTempData assembly means that I’m using a custom TempDataProvider.

        • September 4, 2013 3:59 pm

          Submit an issue to the issue tracker on github and I can look into adding a configuration setting.

  7. jotabe permalink
    February 14, 2013 7:13 pm

    Thanks for this great job! According to your comments, the cookies have a size limit. What’s this limit? Is it fixed or does it depend on each particular browser or configuration? Could your provider check if this limit is surpassed and throw an exception?

  8. Leon Ford permalink
    September 30, 2013 1:30 pm

    I hate to ask, but where’s the documentation on how to use this CookieTempData provider? I have it installed and tried the following code from one of my MVC controllers:

    Dictionary cookies = new Dictionary();
    cookies.Add( “CookieName”, “CookieValue” );
    CookieTempDataProvider cookieContext = new CookieTempDataProvider();
    cookieContext.SaveTempData( ControllerContext, cookies );

    But when I try to “view” my cookies, I don’t see a new cookie with this name.

    • September 30, 2013 9:33 pm

      This blog post is as close as any documentation. If you’d like to hire me, I’d be happy to produce more formal docs :)

      But in short — you don’t need to do anything to use it — it’s automatic. Just reference the NuGet package and it automatically wires itself up as the MVC temp data provider.

      To see it in action, do some HTTP tracing with fiddler or your browser F12 tools.

  9. Bill Moller permalink
    June 24, 2014 2:24 pm

    ViewData does not appear to be serializable… How can any non-in-process ITempDataProvider work for something like this?

  10. September 20, 2014 10:06 pm

    This is cool and, best of all, it’s working. Just a note: I would remove the controller factory because that could cause problems for someone wanting to use a different controller factory. Luckily for me, I am using an umbraco filtered controller factory in the filtered controller factories repository but I think this could cause problems to others.

    • September 21, 2014 10:39 am

      Yea, it’s a good point. IIRC, I think I used it to wrap the existing one, so that’s one solution.

  11. Mehul P. permalink
    January 8, 2015 1:47 pm

    Thanks for this post! One question : Does the provider delete the cookie after the redirect?

    • January 8, 2015 1:49 pm

      If MVC has emptied the temp data, then yes, the cookie will be removed.

  12. Dominic Archual permalink
    February 5, 2015 11:21 am

    Hey Brock; great post. Why did you make the other methods internal instead of private? Load and Save are public, but why should the other methods be accessible from the assembly as opposed to just the class?

    • February 5, 2015 11:43 am

      Not sure — feel free to open an issue or submit a PR on github.

      • Dominic Archual permalink
        February 5, 2015 12:33 pm

        So you are issuing the cookie, but when do you expire it? Isn’t the purpose of TempData to pervade through only the current Http request? Otherwise, why not just use a cookie rather than implementing ITempDataProvider?

  13. Connie DeCinko permalink
    March 10, 2015 7:20 pm

    Brock, I’m looking to solve an issue where my MVC partial view form running in a site with a webforms MasterPage seems to kills the TempData before I can display it. If TempData is empty by time I need to display it, can I assume this cookie solution would have the same issue? The cookie would be empty by time I went to display it on my partial?

  14. Giridharan R permalink
    March 28, 2016 6:11 am

    Getting error after successfully installing package from nuget and overriding the base controller.. When trying to consume tempdata getting error “Type ‘MyProject.Areas.Users.Models.MyViewModel.MyModel’ in Assembly ‘MyProject,Version=1.0.0.0, Culture=neutral, PublicKeyToken=null’ is not marked as serializable”.

Trackbacks

  1. Cookie-based TempData for ASP.NET Core | Luís Gonçalves

Leave a comment