Cookie based TempData provider
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.
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
Is MachineKey.Encode safe to use on .NET 4.0?
@Daniel15, it’s what ASP.NET sits on, so it’s the best game in town (for 4.0).
You are still using MachineKey.Encode here Brock instead of the new MachineKey.Protect. Am I missing something?
I guess the code snippet above is from the 4.0 version. Here’s the 4.5 where I am using Protect/Unprotect:
https://github.com/brockallen/CookieTempData/blob/master/45/BrockAllen.CookieTempData/CookieTempDataProvider.cs
Super as always! I’ll try it tomorrow thanks again
Is MachineKey.Encode safe to use on .NET 4.0?
beautiful solution!
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?
Please post bugs and/or issues here: https://github.com/brockallen/CookieTempData/issues so they can be tracked with the code.
I Installed CookieTempData via Nuget,
How i must config/Enable CookieTempData in My Project(MVC 4 and .Net 4.5)?
It registers itself automatically. If you’re getting an error, submit it to the issues page I linked above.
I use structuremap and also i disabled session in web.config, CookieTempData have problem with these issues?
Sounds like it. Issue a bug, please.
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.
Submit an issue to the issue tracker on github and I can look into adding a configuration setting.
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?
The cookie limit is 4K: http://myownplayground.atspace.com/cookietest.html
And yes, I’ve thought about adding this feature. I’d love if you were to submit an issue so we can all track it: https://github.com/brockallen/CookieTempData/issues
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.
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.
ViewData does not appear to be serializable… How can any non-in-process ITempDataProvider work for something like this?
This approach requires your view data to be serializable.
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.
Yea, it’s a good point. IIRC, I think I used it to wrap the existing one, so that’s one solution.
Thanks for this post! One question : Does the provider delete the cookie after the redirect?
If MVC has emptied the temp data, then yes, the cookie will be removed.
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?
Not sure — feel free to open an issue or submit a PR on github.
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?
I posted an answer to my own question on SO which makes the CookieTempDataProvider work as it should with regard to lifetime of the cookie. http://stackoverflow.com/questions/28351198/implementing-itempdataprovider-vs-using-cookies/28355862#28355862 Thanks again for your notes on security; however, wouldn’t the cookie be encrypted anyway on HTTPS? Isn’t this overkill?
Security is a complex topic. Cookies are protected so the end user doesn’t tamper.
The cookie is cleared when the TempData is set as an empty dictionary by MVC. Notice the code that looks for the empty values and sets the cookie expiration. You shouldn’t be clearing the cookie in your Load API since you don’t know if the app will consume/clear the data or not.
Before I cleared the cookie manually in LoadTempData(), the cookie stayed alive long after the first request. After deleting it, it works as expected. Similar to how in the MVC source, they are calling session.Remove(TempDataSessionStateKey); to remove the session after it is accessed. Ref. https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/SessionStateTempDataProvider.cs
Perhaps they changed the plumbing in MVC4 or 5.
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?
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”.
Right because the data is being serialized.