Skip to content

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 calls ToString().
  • A missing key returns null, so a cast like (int)ViewData["RemainingDays"] throws a NullReferenceException if 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.cshtml for the <title> tag. The page you are reading right now does exactly this.
  • It works in Razor Pages too (PageModel has ViewData).

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 gives null.
  • Runtime errors instead of compile errors. Calling a method on a missing value, e.g. ViewBag.Manager.ToUpper(), throws a RuntimeBinderException ("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 PageModel has ViewData, but no ViewBag.

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" />
What the employee sees
1. Submits the form            -> POST /Leave/Apply
2. Browser is redirected        -> GET  /Leave/History
   "Leave request #1042 for 3 day(s) has been sent to your manager."
3. Presses F5 to refresh        -> GET  /Leave/History
   (no message: it was already read and removed)

The life cycle of a TempData value

StepWhat 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 2Marked 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
CodeReads the value?Available in the next request?
TempData["Key"]YesNo
TempData.Peek("Key")YesYes
TempData["Key"] + TempData.Keep("Key")YesYes
Not read at allNoYes

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 inThe browser (encrypted cookie)Server memory or distributed cache
SetupNoneAddSession, UseSession, AddSessionStateTempDataProvider
SizeKeep it small: cookies are sent with every request and browsers limit them to about 4 KBLarger values are fine
Web farm / multiple serversWorks if all servers share Data Protection keysNeeds 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

PointViewDataViewBagTempData
TypeViewDataDictionarydynamic (wrapper over ViewData)ITempDataDictionary
SyntaxViewData["Key"]ViewBag.KeyTempData["Key"]
LifetimeCurrent request onlyCurrent request onlyUntil read (usually the next request)
Survives a redirect?NoNoYes
StorageMemory, for the requestSame as ViewDataCookie (default) or session
Type casting needed?Yes, for non-string useNo (resolved at runtime)Yes
Compile-time checkingNo (string keys)No (dynamic)No (string keys)
Missing keyReturns nullReturns nullReturns null
Can store objects?Any objectAny objectSimple types only (serialize objects to JSON)
Available in Razor Pages?YesNoYes
Typical usePage title, dropdown lists, small extrasSame as ViewData, shorter syntaxSuccess / 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 RemainingDays and LeaveTypes as properties of an ApplyLeaveViewModel.
  • 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.Peek to 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 Peek or Keep to 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.