ViewBag, ViewData and TempData in ASP.NET Core
In ASP.NET Core MVC, the main way to send data from a controller to a view is a model (ideally a ViewModel). But pages often need a few extra bits that don't belong in the model: the page title, a dropdown list, a count shown in a badge, or a "Saved successfully" message after a form is submitted.
For these small extras, ASP.NET Core gives us three tools: ViewData, ViewBag and TempData. They look similar, and that's why "What is the difference between ViewBag, ViewData and TempData?" is one of the most common MVC interview questions. The real difference comes down to two questions:
- How do you read and write the data? (dictionary syntax or dynamic properties)
- How long does the data live? (only the current request, or until the next request reads it)
Throughout this article we use one realistic example: the Leave Management module of an HR portal, where employees apply for leave and see their leave history.
A quick picture before the code
- ViewData is like a labelled tray that the controller hands to the view. Each item has a label written on it ("Title", "LeaveTypes"). Once the page is delivered, the tray is cleared.
- ViewBag is the same tray; you just pick items up by name (
ViewBag.Title) instead of reading the label (ViewData["Title"]). - TempData is like a parcel locker. You drop a note in it now, and it waits until the next visitor collects it. Once collected, the locker is emptied.
1. ViewData
ViewData is a property of the controller (and of views, layouts and partial views) of type ViewDataDictionary. It is a dictionary: keys are string, values are object. It lives only for the current request.
Example: the "Apply for Leave" page
The page needs a title, a dropdown of leave types and the employee's remaining leave balance:
public class LeaveController : Controller
{
private readonly ILeaveService _leaveService;
public LeaveController(ILeaveService leaveService) => _leaveService = leaveService;
[HttpGet]
public async Task<IActionResult> Apply()
{
int employeeId = User.GetEmployeeId();
ViewData["Title"] = "Apply for Leave";
ViewData["LeaveTypes"] = new SelectList(await _leaveService.GetLeaveTypesAsync(), "Id", "Name");
ViewData["RemainingDays"] = await _leaveService.GetRemainingDaysAsync(employeeId); // e.g. 9
return View(new LeaveRequestForm());
}
}
Reading it in the view (Views/Leave/Apply.cshtml):
@model LeaveRequestForm
<h2>@ViewData["Title"]</h2>
@{
// values come out as object, so cast to the real type before using them as that type
int remaining = (int)(ViewData["RemainingDays"] ?? 0);
}
<p class="badge">You have @remaining day(s) of leave left</p>
<form asp-action="Apply" method="post">
<select asp-for="LeaveTypeId" asp-items="(SelectList)ViewData["LeaveTypes"]"></select>
<input asp-for="FromDate" type="date" />
<input asp-for="ToDate" type="date" />
<textarea asp-for="Reason"></textarea>
<button type="submit">Submit request</button>
</form>
@if (remaining < 2)
{
<p class="warning">Your balance is low. Extra days will be unpaid leave.</p>
}
Key points about ViewData
- Casting is required whenever you need the real type:
(int)ViewData["RemainingDays"],(SelectList)ViewData["LeaveTypes"]. Printing it with@ViewData["Title"]works without a cast because Razor callsToString(). - A missing key returns
null, so a cast like(int)ViewData["RemainingDays"]throws aNullReferenceExceptionif the controller forgot to set it. That's why the example uses?? 0. - Keys are strings, so a typo such as
ViewData["RemainingDay"]is not caught by the compiler. - It flows to the layout and partial views in the same request. That's why
ViewData["Title"]set in a view can be read in_Layout.cshtmlfor the<title>tag. The page you are reading right now does exactly this. - It works in Razor Pages too (
PageModelhasViewData).
The [ViewData] attribute
A controller property marked with [ViewData] is copied into ViewData automatically, using the property name as the key:
public class LeaveController : Controller
{
[ViewData]
public string Title { get; set; } = "";
public IActionResult History()
{
Title = "My Leave History"; // available as ViewData["Title"] in the view and layout
return View();
}
}
See the ViewData in ASP.NET Core lesson for more ViewData examples.
2. ViewBag
ViewBag is a dynamic wrapper around ViewData. Instead of ViewData["RemainingDays"] you write ViewBag.RemainingDays. There is no separate storage: both names point to the same dictionary.
[HttpGet]
public async Task<IActionResult> Apply()
{
int employeeId = User.GetEmployeeId();
ViewBag.Title = "Apply for Leave";
ViewBag.LeaveTypes = new SelectList(await _leaveService.GetLeaveTypesAsync(), "Id", "Name");
ViewBag.RemainingDays = await _leaveService.GetRemainingDaysAsync(employeeId);
return View(new LeaveRequestForm());
}
<h2>@ViewBag.Title</h2>
<p>You have @ViewBag.RemainingDays day(s) of leave left</p>
@if (ViewBag.RemainingDays < 2) @* no cast needed: dynamic is resolved at runtime *@
{
<p class="warning">Your balance is low.</p>
}
Proof that ViewBag and ViewData share the same data
public IActionResult Demo()
{
ViewData["Department"] = "Finance";
ViewBag.Manager = "Neha Sharma";
return View();
}
<p>@ViewBag.Department</p> <!-- Finance (set through ViewData) -->
<p>@ViewData["Manager"]</p> <!-- Neha Sharma (set through ViewBag) -->
So if you set the same key through both, the last one wins. Choosing between them is only about syntax, not storage or lifetime.
Key points about ViewBag
- No casting is needed for simple use, and the code looks cleaner.
- No compile-time checking and no IntelliSense.
ViewBag.RemainingDay(missing "s") compiles fine and quietly givesnull. - Runtime errors instead of compile errors. Calling a method on a missing value, e.g.
ViewBag.Manager.ToUpper(), throws aRuntimeBinderException("Cannot perform runtime binding on a null reference"). - Extension methods don't work with dynamic values.
@Html.Partial("_LeaveSummary", ViewBag.Summary)gives compile error CS1973. Cast first:(LeaveSummary)ViewBag.Summary. - It is slightly slower than ViewData because of dynamic binding, though the difference rarely matters in a web page.
- Not available in Razor Pages. A
PageModelhasViewData, but noViewBag.
See the ViewBag in ASP.NET Core lesson for more ViewBag examples.
The redirect problem: why ViewData and ViewBag are not enough
When the employee submits the leave form, a good application follows the Post/Redirect/Get (PRG) pattern: save the data in the POST action, then redirect to another page. This stops the browser from submitting the form again if the user presses F5.
Let's try to show a success message with ViewBag:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Apply(LeaveRequestForm form)
{
if (!ModelState.IsValid)
return View(form);
int requestId = await _leaveService.SubmitAsync(User.GetEmployeeId(), form);
ViewBag.Message = $"Leave request #{requestId} submitted."; // set in THIS request...
return RedirectToAction(nameof(History)); // ...but the browser makes a NEW request
}
public IActionResult History()
{
// ViewBag.Message is null here. The message is lost.
return View();
}
A redirect sends a 302 response to the browser, and the browser then makes a brand-new request to /Leave/History. ViewData and ViewBag belonged to the first request, so they are gone. This is exactly the problem TempData solves.
3. TempData
TempData (type ITempDataDictionary) is also a string-keyed dictionary, but it is stored between requests. Data you put in TempData stays available until it is read, usually in the very next request, and is then removed automatically.
Example: the success message after submitting leave
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Apply(LeaveRequestForm form)
{
if (!ModelState.IsValid)
return View(form);
int requestId = await _leaveService.SubmitAsync(User.GetEmployeeId(), form);
int days = (form.ToDate - form.FromDate).Days + 1;
TempData["SuccessMessage"] = $"Leave request #{requestId} for {days} day(s) has been sent to your manager.";
return RedirectToAction(nameof(History));
}
public async Task<IActionResult> History()
{
var requests = await _leaveService.GetHistoryAsync(User.GetEmployeeId());
return View(requests);
}
Instead of reading the message on every page, most real projects show it from one partial view placed in the layout, so any action can use it:
@* Views/Shared/_Alerts.cshtml *@
@if (TempData["SuccessMessage"] is string success)
{
<div class="alert alert-success">@success</div>
}
@if (TempData["ErrorMessage"] is string error)
{
<div class="alert alert-danger">@error</div>
}
@* Views/Shared/_Layout.cshtml, just above RenderBody() *@
<partial name="_Alerts" />
The life cycle of a TempData value
| Step | What happens |
|---|---|
| Request 1 (POST) | The value is written: TempData["SuccessMessage"] = "...". At the end of the request it is saved (by default, in a cookie). |
| Request 2 (GET after redirect) | The value is loaded. When the view reads it, it is marked for deletion. |
| End of request 2 | Marked values are removed. Values that were not read are kept for a later request. |
The important rule: TempData is removed after it is read, not after a fixed number of requests. If nobody reads it, it stays.
Peek and Keep: reading without removing
Sometimes you need to read a value and still keep it for the next request. Example: the leave request is waiting for manager approval, and the "pending" banner must stay visible across two pages.
// Peek: read WITHOUT marking for deletion
string? note = TempData.Peek("PendingNote") as string;
// Keep: read normally, then cancel the deletion
string? note2 = TempData["PendingNote"] as string;
TempData.Keep("PendingNote"); // keep only this key
TempData.Keep(); // or keep ALL values
| Code | Reads the value? | Available in the next request? |
|---|---|---|
TempData["Key"] | Yes | No |
TempData.Peek("Key") | Yes | Yes |
TempData["Key"] + TempData.Keep("Key") | Yes | Yes |
| Not read at all | No | Yes |
Where is TempData stored?
By default ASP.NET Core stores TempData in a cookie named .AspNetCore.Mvc.CookieTempDataProvider. The cookie is encrypted with ASP.NET Core Data Protection, so the user can't read or change it. No extra setup is needed: AddControllersWithViews() already registers it.
You can switch to session storage, which keeps the data on the server instead of in the browser:
builder.Services.AddControllersWithViews()
.AddSessionStateTempDataProvider();
builder.Services.AddSession();
var app = builder.Build();
app.UseSession(); // before MapControllerRoute
| Cookie provider (default) | Session provider | |
|---|---|---|
| Data stored in | The browser (encrypted cookie) | Server memory or distributed cache |
| Setup | None | AddSession, UseSession, AddSessionStateTempDataProvider |
| Size | Keep it small: cookies are sent with every request and browsers limit them to about 4 KB | Larger values are fine |
| Web farm / multiple servers | Works if all servers share Data Protection keys | Needs a distributed session store (Redis, SQL Server) |
Cookie consent: if your app uses a cookie consent banner (CheckConsentNeeded), the TempData cookie is treated as non-essential and is not written until the user accepts cookies. Your success messages will silently disappear. Mark it as essential if you need it: builder.Services.Configure<CookieTempDataProviderOptions>(o => o.Cookie.IsEssential = true);
Storing objects in TempData
TempData can only store simple values: strings, numbers, bool, DateTime, Guid and arrays of these. Putting a class object in it throws an InvalidOperationException when the data is saved, saying the TempData serializer cannot serialize that type. The solution is to store the object as JSON:
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
public static class TempDataExtensions
{
public static void Put<T>(this ITempDataDictionary tempData, string key, T value) =>
tempData[key] = JsonSerializer.Serialize(value);
public static T? Get<T>(this ITempDataDictionary tempData, string key) =>
tempData.TryGetValue(key, out object? stored) && stored is string json
? JsonSerializer.Deserialize<T>(json)
: default;
}
// POST action
TempData.Put("LastRequest", new LeaveSummary(requestId, form.FromDate, form.ToDate, "Pending"));
// next request
LeaveSummary? summary = TempData.Get<LeaveSummary>("LastRequest");
The [TempData] attribute
Like [ViewData], a controller property can be backed by TempData:
public class LeaveController : Controller
{
[TempData]
public string? SuccessMessage { get; set; } // stored as TempData["SuccessMessage"]
[HttpPost]
public async Task<IActionResult> Cancel(int id)
{
await _leaveService.CancelAsync(id);
SuccessMessage = $"Leave request #{id} was cancelled.";
return RedirectToAction(nameof(History));
}
}
ViewBag vs ViewData vs TempData: comparison table
| Point | ViewData | ViewBag | TempData |
|---|---|---|---|
| Type | ViewDataDictionary | dynamic (wrapper over ViewData) | ITempDataDictionary |
| Syntax | ViewData["Key"] | ViewBag.Key | TempData["Key"] |
| Lifetime | Current request only | Current request only | Until read (usually the next request) |
| Survives a redirect? | No | No | Yes |
| Storage | Memory, for the request | Same as ViewData | Cookie (default) or session |
| Type casting needed? | Yes, for non-string use | No (resolved at runtime) | Yes |
| Compile-time checking | No (string keys) | No (dynamic) | No (string keys) |
| Missing key | Returns null | Returns null | Returns null |
| Can store objects? | Any object | Any object | Simple types only (serialize objects to JSON) |
| Available in Razor Pages? | Yes | No | Yes |
| Typical use | Page title, dropdown lists, small extras | Same as ViewData, shorter syntax | Success / error messages after a redirect (PRG) |
Which one should I use?
- The main data of the page: neither. Use a strongly typed model or ViewModel. You get IntelliSense, compile-time checks and easy unit testing. For example, put
RemainingDaysandLeaveTypesas properties of anApplyLeaveViewModel. - Small extras for the current page (title, a flag for the layout, a badge count): ViewData. Prefer it over ViewBag in team projects because it also works in Razor Pages and is a little more explicit.
- ViewBag: fine for quick, simple pages if your team likes the shorter syntax, but never for large or important data.
- A message or value that must survive a redirect: TempData.
Common mistakes in real projects
- Using ViewBag to show a message after
RedirectToAction. The message never appears. Use TempData. - Reading TempData in the controller "just to check" it. Reading marks it for deletion, so the view finds nothing. Use
TempData.Peekto check. - Putting large data or whole lists in TempData with the cookie provider. The cookie grows, is sent with every request, and can exceed browser limits.
- Storing a class object directly in TempData. It throws when TempData is saved; serialize to JSON first.
- Spelling a key differently in controller and view (
"LeaveTypes"vs"LeaveType"). Define keys as constants, or move the data into a ViewModel. - Using ViewBag for everything. The view becomes full of hidden dependencies nobody can find with "Find all references".
Interview questions
1. What is the difference between ViewData and ViewBag?
Both store data for the current request and share the same underlying dictionary. ViewData uses string keys and needs casting; ViewBag is a dynamic wrapper over ViewData, so it uses property syntax and needs no cast, but it has no compile-time checking. ViewData is available in Razor Pages; ViewBag is not.
2. Why do we need TempData if we already have ViewData?
ViewData and ViewBag are lost when you redirect, because a redirect causes a new HTTP request. TempData is stored between requests (in a cookie or session), so it survives the redirect. It is mainly used for messages in the Post/Redirect/Get pattern.
3. How long does TempData keep its value?
Until it is read. When a value is read, it is marked for deletion and removed at the end of that request. Values that are not read stay available for later requests.
4. What is the difference between TempData.Peek and TempData.Keep?
Peek("key") reads a value without marking it for deletion. Keep("key") is called after a normal read to cancel the deletion; Keep() without a key keeps all values.
5. Where is TempData stored in ASP.NET Core?
By default in an encrypted cookie (CookieTempDataProvider). It can be stored in session by calling AddSessionStateTempDataProvider() and enabling session middleware.
6. Can I store a complex object in TempData?
Not directly. TempData supports only simple types. Serialize the object to a JSON string before storing and deserialize after reading.
7. If I set ViewData["Name"] and read ViewBag.Name, what do I get?
The same value, because ViewBag is only a dynamic view over the ViewData dictionary.
8. Which is faster, ViewData or ViewBag?
ViewData is slightly faster because ViewBag uses dynamic binding at runtime. In practice the difference is tiny compared to a database call or rendering the page, so choose based on readability and safety.
9. Can a layout page read ViewData set in a controller or view?
Yes. ViewData is shared by the view, its layout and its partial views during the same request. This is how the page title is usually passed to the layout.
10. Should I use ViewBag or a ViewModel to pass data to a view?
A ViewModel, for anything important. It is strongly typed, checked by the compiler, supports IntelliSense and is easy to test. Keep ViewData/ViewBag for small extras such as the page title.
Summary
- ViewData: a string-keyed dictionary for the current request; needs casting.
- ViewBag: a dynamic wrapper over the same ViewData; no casting, no compile-time checks, not in Razor Pages.
- TempData: survives a redirect and is removed after it is read; stored in a cookie by default.
- Use
PeekorKeepto read TempData without losing it. - Store only simple values in TempData; serialize objects to JSON.
- Use a strongly typed ViewModel for the main page data, ViewData for small extras, and TempData for post-redirect messages.