ASP.NET MVC: Do You Know Where Your TempData Is?

I recently discovered that despite the fact that I’d been using the TempData dictionary in my applications, I didn’t really have a full grasp on what it was doing behind the scenes. Of course this meant learning the lesson the hard way once something stopped working like I thought it would. I only really had myself to blame since in the end it was a simple case of RTFM, but it seemed like a good opportunity to dig in and see how TempData really works.

What is TempData?

Let’s start by describing what TempData gives you. Basically, it’s a bucket where you can dump data that is only needed for the following request. That is, anything you put into TempData is discarded after the next request completes. This is useful for one-time messages, such as form validation errors. The important thing to take note of there is that this applies to the next request in the session, so that request can potentially happen in a different browser window or tab. If you need something more persistent, Session is likely what you’re looking for.

Where Is TempData Stored?

This is the part that came back to bite me. By default, TempData is stored in the session. Yes, the session! Most of the time this probably doesn’t matter to you, since as long as you get your objects back when you want them you have no reason to worry about where it was kept. However, let’s say you decide you want to switch away from the default Session-State Mode, and use State Server Mode or SQL Server Mode. These modes require that all objects stored in session be serializable. This is exactly what I did, and without knowing that TempData used the session, it was non-obvious why I started seeing session errors.

You might have noticed that I said that TempData is kept in the session by DEFAULT, meaning you are not tied to that if you decide you don’t like it that way. Let’s take a look at some of the source code to ASP.NET MVC 2 and see how things work.

ITempDataProvider and SessionStateTempDataProvider
In the System.Web.Mvc namespace you’ll find a very simple interface called ITempDataProvider that looks like this:

public interface ITempDataProvider {
    IDictionary<string, object> LoadTempData(ControllerContext controllerContext);
    void SaveTempData(ControllerContext controllerContext,
                                IDictionary<string, object> values);
}

Not much to see there. It simply defines the methods of saving and loading the dictionary. In the same namespace is SessionStateTempDataProvider which implements ITempDataProvider. When loading the dictionary out of the session, it explicitly removes it afterwards to make sure only one request gets access to it:

public virtual IDictionary<string, object> LoadTempData(
    ControllerContext controllerContext) {

    HttpSessionStateBase session = controllerContext.HttpContext.Session;

    if (session != null) {
        Dictionary<string, object> tempDataDictionary =
            session[TempDataSessionStateKey] as Dictionary<string, object>;

        if (tempDataDictionary != null) {
            // If we got it from Session, remove it so that no other request gets it
            session.Remove(TempDataSessionStateKey);
            return tempDataDictionary;
        }
    }

    return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}

The controller object has a public property for the TempData provider which that uses the session provider by default:

public ITempDataProvider TempDataProvider {
    get {
        if (_tempDataProvider == null) {
            _tempDataProvider = CreateTempDataProvider();
        }
        return _tempDataProvider;
    }
    set {
        _tempDataProvider = value;
    }
}

protected virtual ITempDataProvider CreateTempDataProvider() {
    return new SessionStateTempDataProvider();
}

How the TempData Provider Gets Used

If you want to tell your controllers to use a different provider, you set it up in your controller’s constructor and you’re good to go. You might have also noticed that the LoadTempData(ControllerContext) function in ITempDataProvider returns an IDictionary, but the TempData property on the controller (actually, ControllerBase to be specific), is of type TempDataDictionary. This is another MVC class that wraps around an IDictionary and provides all the functionality we need from TempData. This is a big class so I won’t go over everything, but there are a couple parts to highlight. First, you can see that the class is marked as being serializable, so that it can properly be put into session:

[Serializable]
    public class TempDataDictionary : IDictionary<string, object>, ISerializable {

Second, there is the Load(ControllerContext, ITempDataProvider) function that uses the specified provider to hydrate the dictionary:

public void Load(ControllerContext controllerContext,
                      ITempDataProvider tempDataProvider) {
    IDictionary<string, object> providerDictionary =
        tempDataProvider.LoadTempData(controllerContext);

    _data = (providerDictionary != null)
        ? new Dictionary<string, object>(
              providerDictionary, StringComparer.OrdinalIgnoreCase) :
        new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);

    _initialKeys = new HashSet<string>(
                      _data.Keys, StringComparer.OrdinalIgnoreCase);

    _retainedKeys.Clear();
}

Similarly, it also has a Save(ControllerContext, ITempDataProvider) function that uses the provider to save out the new dictionary:

public void Save(ControllerContext controllerContext,
                       ITempDataProvider tempDataProvider) {

    string[] keysToKeep =
        _initialKeys
            .Union(_retainedKeys, StringComparer.OrdinalIgnoreCase)
            .ToArray();

    string[] keysToRemove =
        _data
            .Keys
                .Except(keysToKeep, StringComparer.OrdinalIgnoreCase)
                .ToArray();

    foreach (string key in keysToRemove) {
        _data.Remove(key);
    }

    tempDataProvider.SaveTempData(controllerContext, _data);
}

The only part left is figuring when in the pipeline TempData gets load, and then when it gets saved out again. The answers to both are found in the ExecuteCore() method in Controller. This method gets called by MVC to invoke the current action. In order, it loads TempData, then executes the current action, then saves out the new dictionary:

protected override void ExecuteCore() {
    PossiblyLoadTempData();

    try {
        string actionName = RouteData.GetRequiredString("action");

        if (!ActionInvoker.InvokeAction(ControllerContext, actionName)) {
            HandleUnknownAction(actionName);
        }
    }
    finally {
        PossiblySaveTempData();
    }
}

ASP.NET MVC 2 introduced the concept of RenderAction, where you could call out from a view to another controller action and render it within that view. If you do this, the child action actually shares TempData with the parent request. I have found this useful in passing something down to a child request to avoid having to redo a database call for something needed by both the parent and the child. This logic is part of the PossiblyLoadTempData() and PossiblySaveTempData() functions referenced in ExecureCore():

internal void PossiblyLoadTempData() {
    if (!ControllerContext.IsChildAction) {
        TempData.Load(ControllerContext, TempDataProvider);
    }
}

internal void PossiblySaveTempData() {
    if (!ControllerContext.IsChildAction) {
        TempData.Save(ControllerContext, TempDataProvider);
    }
}

Those are the highlights of how TempData works under the hood, and how you can plug in a different provider to tailor the behavior to what you want. You can also find references to it outside of the controller in other places where you have access to TempData, such as ViewContext, ViewPage, etc. As of right now the session provider is the only one in the main assembly. The MvcFutures assembly includes the CookieTempDataProvider class which provides cookie-based TempData storage.

A MongoDB TempData Provider

I think we’ve looked at enough ASP.NET MVC code now, so let’s try writing a custom provider. For this example I’m going to store the data in a MongoDB collection. Now, keep in mind that this example is meant to show how to implement a custom provider, so I’m not trying to make a case that you should store your TempData in MongoDB. For this example I’m going to use the MongoDB-CSharp driver.

First, let’s define the model object that we’re going to store in MongoDB.

public class MongoTempData
{
    public string SessionIdentifier { get; set; }

    public string Key { get; set; }

    public object Value { get; set; }
}

Now we can define our TempData provider to store that model. To avoid sending a user someone else’s TempData, we’ll need a way of uniquely identifying users. Since this is a crude demo, I’ll just use the user’s host address. Obviously, don’t do this in production code.

public class MongoTempDataProvider : ITempDataProvider
{
    private string _collectionName;
    private string _databaseName;

    public MongoTempDataProvider(string databaseName, string collectionName)
    {
        _collectionName = collectionName;
        _databaseName = databaseName;
    }
}

Now for the function for loading data from the collection. Since I’m not specifying the hostname or port for the database, it assumes a default of localhost and 27017. Different values can be provided via the contructors to the Mongo object.

public IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
{
    var tempDataDictionary = new Dictionary<string, object>();

    using (Mongo mongo = new Mongo())
    {
        mongo.Connect();

        IMongoCollection<MongoTempData> collection =
            mongo
                .GetDatabase(_databaseName)
                .GetCollection<MongoTempData>(_collectionName);

        IEnumerable<MongoTempData> tempData = collection.Find(item =>
            item.SessionIdentifier ==
                controllerContext.HttpContext.Request.UserHostAddress
        ).Documents;

        foreach (var tempDataItem in tempData)
        {
            tempDataDictionary.Add(tempDataItem.Key, tempDataItem.Value);

            collection.Remove(tempDataItem);
        }
    }

    return tempDataDictionary;
}

That will connect to the database and read in any TempData that is queued up for the current user. Once an item is read into the dictionary it gets removed from the collection, to make sure no other requests can get it.

Now, the save method:

public void SaveTempData(ControllerContext controllerContext,
                                    IDictionary<string, object> values)
{
    using (Mongo mongo = new Mongo())
    {
        mongo.Connect();

        IMongoCollection<MongoTempData> collection =
            mongo
                .GetDatabase(_databaseName)
                .GetCollection<MongoTempData>(_collectionName);

        IEnumerable<MongoTempData> oldItems =
            collection.Find(item =>
                item.SessionIdentifier ==
                    controllerContext.HttpContext.Request.UserHostAddress
            ).Documents;

        foreach (var tempDataItem in oldItems)
        {
            collection.Remove(tempDataItem);
        }

        if (values != null && values.Count > 0)
        {
            collection.Insert(
                values.Select(tempDataValue =>
                    new MongoTempData
                    {
                        SessionIdentifier =
                            controllerContext.HttpContext.Request.UserHostAddress,
                        Key = tempDataValue.Key,
                        Value = tempDataValue.Value
                    }
                )
            );
        }
    }
}

That will clear out any old data in the dictionary and replace it with the new values. That’s all the code required for the provider, so now we just need to plug it into the controller. We’ll create a base controller class that our controllers will inherit from, to avoid having to specify the TempDataProvider in every controller.

public abstract class MongoControllerBase : Controller
{
    public MongoControllerBase()
    {
        TempDataProvider = new MongoTempDataProvider("test", "MongoTempData");
    }
}

Next we’ll define a test controller with two actions. The first action will insert some data into TempData, and the second will print out the contents of TempData. Remember that the controller needs to inherit from our base controller in order to use our custom provider. If you don’t, it will default to using the session provider.

public class HomeController : MongoControllerBase
{
    public ActionResult Index()
    {
        TempData["CurrentDateTime"] = DateTime.Now;
        TempData["MeaningOfLife"] = 42;

        return Content("TempData Updated");
    }

    public ActionResult ReadTempData()
    {
        return View();
    }
}

Here’s our view for ReadTempData:

Contents of TempData:

<br />
<br />

<% foreach (var tempDataItem in TempData)
   { %>

    Key: <%: tempDataItem.Key %>; Value: <%: tempDataItem.Value %>
    <br />

<% } %>

If you fire up the application, you should see text that says “TempData Updated”. Now if you hit /Home/ReadTempData, you should see something that looks like:

Contents of TempData:

Key: CurrentDateTime; Value: 7/14/2010 10:52:45 PM
Key: MeaningOfLife; Value: 42

If you refresh the page, you will see that the contents of TempData are now empty.

For me, this was one big reminder that you should know exactly how something works before making assumptions about it. Since ASP.NET MVC is open source, it was very easy to load up the solution and poke around under the hood and see precisely how it behaves.

comments powered by Disqus
Navigation