Inner-workings of TempData in ASP.NET MVC 5

csharp, development, asp.net, mvc comments edit

In ASP.NET, TempData is one of the mechanisms used to pass data from controller to the view. In this post I’ll dive into its source code to investigate its behaviour.

What is TempData

TempData is an instance of TempDataDictionary that is used to pass data from the controller to the view.

Lifespan

The lifespan of TempData is rather unusual: It lives for one request only. In order to achieve this it maintains two HashSets to manage keys as well as the data dictionary:

private Dictionary<string, object> _data;
private HashSet<string> _initialKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private HashSet<string> _retainedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

When we read some data using an indexer or TryGetValue method it removes that key from _initalKeys collection.

public bool TryGetValue(string key, out object value)
{
    _initialKeys.Remove(key);
    return _data.TryGetValue(key, out value);
}

The actual dictionary that holds the data is intact at this point. That’s why we can read same data consecutively without any issues. It only removes the key from _initialKeys collection, basically marking it to be deleted when the data is persisted.

Peek and keep

If we want the values in TempData last longer we can use Peek and Keep methods. What Peek does is return the value without removing it from the _initialKeys:

public object Peek(string key)
{
    object value;
    _data.TryGetValue(key, out value);
    return value;
}

Alternatively, we can call Keep method. Similarly it doesn’t manipulate the data directly but just marks the key to be persisted by adding it to the _retainedKeys collection.

public void Keep(string key)
{
    _retainedKeys.Add(key);
}

Parameterless overload of Keep method add all keys in the _data dictionary to _retainedKeys.

Which keys to persist

So as seen above when we get values and call Peek/Keep methods, operations are carried out on _initialKeys and _retainedKeys collections and nothing happens to the actual data. These operations take effect when the _data is actually saved:

public void Save(ControllerContext controllerContext, ITempDataProvider tempDataProvider)
{
    _data.RemoveFromDictionary((KeyValuePair<string, object> entry, TempDataDictionary tempData) =>
        {
            string key = entry.Key;
            return !tempData._initialKeys.Contains(key) 
                && !tempData._retainedKeys.Contains(key);
        }, this);

    tempDataProvider.SaveTempData(controllerContext, _data);
}

Before the data is passed on to the provider it’s pruned. The keys that don’t exist in the _retainedKeys (the keys we explicitly told to keep) and _initialKeys (the keys that have not been touched so far or accessed through Peek method) collections are removed.

Providers

By default, TempData uses session variables to persist data from one request to the next. Serializing and deserializing data is carried out via an object implementing ITempDataProvider. By default SessionStateTempDataProvider class is used to provide this functionality. It occurs in the CreateTempDataProvider method in Controller.cs class:

protected virtual ITempDataProvider CreateTempDataProvider()
{
    return Resolver.GetService<ITempDataProvider>() ?? new SessionStateTempDataProvider();
}

This also means we can replace the provider with our own custom class. For demonstration purposes I wrote my own provider which uses a simple text file to persist TempData:

public class TextFileTempDataProvider : ITempDataProvider
{
    internal readonly string FileName = Path.Combine(HttpContext.Current.Server.MapPath(@"~/App_Data"), @"TempData.txt");

    public IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
    {
        if (File.Exists(FileName))
        {
            string json = File.ReadAllText(FileName);
            return Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
        }

        return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
    }
    
    public void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
    {
        string json = Newtonsoft.Json.JsonConvert.SerializeObject(values);
        File.WriteAllText(FileName, json);
    }
}

In order to use this class it needs to be assigned to TempDataProvider in the controller’s constructor

public FirstController()
{
    TempDataProvider = new TextFileTempDataProvider();
}

Of course it’s not a bright idea to use disk for such operations, this is just for demonstration purposes and makes it easier to observe the behaviour.

Conclusion

Often times I’ve found knowledge about the internals of a construct useful. Applications and frameworks are getting more complex each day, adding more layers and hiding the complexity from the consumers. It’s great because we can focus on the actual business logic and application we are building but when we get stuck it takes quite a while to figure out what’s going on. Having in-depth knowledge on the internals can save a lot if time.

Resources

Comments