HTTP is stateless. Every request is a stranger. So how does your application remember a success notification after a form submission redirects the user? That's the exact problem TempData was built to solve — and understanding it deeply separates a junior developer from a production-ready engineer.
TempData is a short-lived, dictionary-based storage mechanism in ASP.NET Core MVC that allows you to pass data from one HTTP request to the very next request — and then it self-destructs. Think of it as a sticky note you hand to the next person in line, and once they read it, it dissolves.
Internally, TempData implements ITempDataDictionary, which inherits from IDictionary<string, object>. It stores values as object, meaning you must cast back to the original type when reading.
The real-world reason TempData exists is the Post-Redirect-Get (PRG) Pattern. Consider this real production scenario:
/orders (GET request) — this prevents duplicate form submissions on refreshThe Problem: Step 3 triggers a brand-new HTTP request. ViewData and ViewBag from Step 2 are completely gone — they don't survive across requests. You need a mechanism to pass that success message across the redirect boundary.
The Solution: TempData. It stores the data in a cookie (default) or session, and it survives precisely until it is read in the next request.
ASP.NET Core provides two TempData Providers — the mechanism that physically stores and retrieves TempData between requests:
AddSession() and UseSession() middleware setup// Program.cs — Switching to Session-based TempData
builder.Services.AddControllersWithViews()
.AddSessionStateTempDataProvider(); // Switch from cookie to session
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
var app = builder.Build();
app.UseSession(); // Must come BEFORE UseRouting
app.UseRouting();
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
// OrdersController.cs — Production-grade POST handler
public class OrdersController : Controller
{
private readonly IOrderService _orderService;
private readonly ILogger<OrdersController> _logger;
public OrdersController(IOrderService orderService, ILogger<OrdersController> logger)
{
_orderService = orderService;
_logger = logger;
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> PlaceOrder(OrderViewModel model)
{
if (!ModelState.IsValid)
return View("Checkout", model);
try
{
var orderId = await _orderService.CreateOrderAsync(model);
// Store notification message in TempData — survives the redirect
TempData["Notification"] = $"Order #{orderId} placed successfully! 🎉";
TempData["NotificationType"] = "success";
_logger.LogInformation("Order {OrderId} created by user {UserId}", orderId, User.Identity.Name);
return RedirectToAction(nameof(Confirmation), new { id = orderId });
}
catch (InsufficientStockException ex)
{
TempData["Notification"] = $"Item '{ex.ProductName}' is out of stock.";
TempData["NotificationType"] = "danger";
return RedirectToAction(nameof(Index));
}
}
public IActionResult Confirmation(int id)
{
// TempData["Notification"] is automatically available in the view
var order = _orderService.GetOrderById(id);
return View(order);
}
}
<!-- _Layout.cshtml — Global notification renderer -->
@if (TempData["Notification"] != null)
{
var alertType = TempData["NotificationType"]?.ToString() ?? "info";
<div class="alert alert-@alertType alert-dismissible fade show" role="alert">
<strong>@(alertType == "success" ? "✅" : "⚠️")</strong>
@TempData["Notification"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
// ProductsController.cs — Clean, type-safe approach
public class ProductsController : Controller
{
// Decorated properties auto-sync with TempData dictionary
[TempData]
public string StatusMessage { get; set; }
[TempData]
public string StatusType { get; set; }
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id)
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
{
StatusMessage = "Product not found.";
StatusType = "warning";
return RedirectToAction(nameof(Index));
}
await _repository.DeleteAsync(id);
StatusMessage = $"Product '{product.Name}' has been permanently deleted.";
StatusType = "danger";
return RedirectToAction(nameof(Index));
}
}
// TempData can only store primitive types natively.
// For complex objects, serialize to JSON first:
using System.Text.Json;
// SETTING complex data
var orderSummary = new OrderSummary { OrderId = 4521, Total = 149.99m, ItemCount = 3 };
TempData["OrderSummary"] = JsonSerializer.Serialize(orderSummary);
// READING complex data (in the next request)
if (TempData["OrderSummary"] is string json)
{
var summary = JsonSerializer.Deserialize<OrderSummary>(json);
// Use summary.OrderId, summary.Total, etc.
}
// PRO TIP: Create extension methods for cleaner usage
public static class TempDataExtensions
{
public static void Set<T>(this ITempDataDictionary tempData, string key, T value)
=> tempData[key] = JsonSerializer.Serialize(value);
public static T? Get<T>(this ITempDataDictionary tempData, string key)
{
if (tempData.TryGetValue(key, out var obj) && obj is string json)
return JsonSerializer.Deserialize<T>(json);
return default;
}
}
By default, reading TempData marks it for deletion. Sometimes you need to read without consuming, or extend its life for one more request:
Reads the value but does NOT mark it for deletion. The data remains available for the subsequent request.
// Value survives to the next request
var msg = TempData.Peek("StatusMessage");
// msg has the value, but TempData["StatusMessage"]
// will STILL be available in the next request
If you've already read the value (which marks it for deletion), calling Keep() cancels the deletion for the next request.
// Step 1: Read (auto-marks for deletion)
var msg = TempData["StatusMessage"];
// Step 2: Cancel deletion — keep for one more request
TempData.Keep("StatusMessage");
// Or keep ALL TempData items
TempData.Keep();
| Feature | ViewData | ViewBag | TempData |
|---|---|---|---|
| Type | Dictionary<string, object> | dynamic wrapper | Dictionary<string, object> |
| Scope | Current request only | Current request only | Current + next request |
| Survives Redirect? | ❌ No | ❌ No | ✅ Yes |
| Type-Safe? | No (requires casting) | No (dynamic) | No (requires casting) |
| Storage | In-memory (request) | In-memory (request) | Cookie or Session |
| Best For | Page titles, metadata | Quick ad-hoc data | Flash messages after redirect |
| Read Behavior | Unlimited reads | Unlimited reads | Auto-deleted after first read |
[TempData] attribute to avoid magic string keys_Layout.cshtml for global visibilityTempData auto-deletes on read — use Peek/Keep if neededIf you store too much data in TempData with the default cookie provider, the response header exceeds the browser's cookie limit (~4KB). The request will silently fail with no error message — the data just vanishes. In production, always validate payload size or switch to the Session provider for larger payloads.
Q: "When would you use TempData instead of Session in a production application?"
Architect Answer: "I use TempData exclusively for ephemeral notification messages following the Post-Redirect-Get pattern — success alerts, form validation summaries, or one-time warnings. TempData self-destructs after being read, so I never have to worry about cleanup or stale data. For anything that needs to persist across multiple requests — like a multi-step wizard, a shopping cart, or user preferences — I use Session backed by a distributed cache like Redis. The key architectural distinction is: TempData is for one-time messages, Session is for multi-request state, and a database is for persistent state."