Tutorials ASP.NET Core MVC Mastery
TempData
On this page
TempData in ASP.NET Core MVC β The Complete Production Guide
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.
1. WHAT is TempData?
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 Core Mental Model
- ViewData / ViewBag β Live for the current request only (Controller β View)
- TempData β Survives exactly ONE redirect (Controller β Redirect β Next Controller/View)
- Session β Persists across many requests until timeout or explicit removal
2. WHY Do We Need TempData? The PRG Problem
The real-world reason TempData exists is the Post-Redirect-Get (PRG) Pattern. Consider this real production scenario:
- User fills out an order form and clicks Submit (POST request)
- Server processes the order successfully
- Server redirects user to
/orders(GET request) β this prevents duplicate form submissions on refresh - The orders page needs to show: "Order #4521 placed successfully!"
The 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.
3. HOW TempData Works Under the Hood
ASP.NET Core provides two TempData Providers β the mechanism that physically stores and retrieves TempData between requests:
Cookie-Based Provider (Default)
- TempData values are serialized to JSON, then encrypted using ASP.NET Core's Data Protection API
- Stored as a cookie on the client's browser
- No server-side state needed β perfect for web farms and load balancers
- Limitation: Cookie size limit (~4KB). Don't store large objects
Session-Based Provider
- Stores TempData on the server-side in session storage
- Requires
AddSession()andUseSession()middleware setup - Can handle larger payloads than cookies
- Trade-off: Requires sticky sessions in web farms unless using distributed session (Redis)
Switching to Session-Based TempData Provider:
// 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?}");
4. REAL-TIME PRODUCTION EXAMPLES
Example 1: E-Commerce Order Notification (PRG Pattern)
// 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);
}
}
Rendering Notifications Globally (in _Layout.cshtml):
<!-- _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>
}
Example 2: Using [TempData] Attribute β Eliminate Magic Strings
// 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));
}
}
Example 3: Passing Complex Objects via JSON Serialization
// 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;
}
}
5. Keep() and Peek() β Controlling TempData Lifetime
By default, reading TempData marks it for deletion. Sometimes you need to read without consuming, or extend its life for one more request:
Peek(key) β Read Without Consuming
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
Keep(key) β Retain After Reading
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();
6. ViewData vs ViewBag vs TempData β The Complete Comparison
| 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 |
7. Best Practices & Production Guidelines
β DO
- Use TempData only for flash notifications after PRG redirects
- Use the
[TempData]attribute to avoid magic string keys - Create extension methods for complex object serialization
- Place notification rendering in
_Layout.cshtmlfor global visibility - Keep payloads small (< 4KB for cookie provider)
β DON'T
- Don't use TempData as a substitute for Session or database storage
- Don't store large objects or lists β serialize to JSON if needed
- Don't store sensitive data (passwords, tokens) β it's in a cookie!
- Don't rely on TempData for multi-step wizards β use Session or DB instead
- Don't forget that
TempDataauto-deletes on read β use Peek/Keep if needed
Production Gotcha: Cookie Size Overflow
If 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.
8. Interview Mastery
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."