TempData, ViewBag and ViewData — Passing Data to Views

Controllers need to pass data to views. The primary mechanism is the strongly-typed ViewModel passed to View(model) — this is always the preferred approach. But ASP.NET Core also provides three convenience mechanisms for passing auxiliary data: ViewBag (dynamic), ViewData (dictionary), and TempData (survives one redirect). Understanding when each is appropriate and when to reach for a ViewModel instead prevents scattered, hard-to-maintain data passing patterns.

ViewBag and ViewData

// ── ViewData — dictionary, string keys, object values ─────────────────────
// Lives for ONE view render (not across redirects)
public IActionResult Index()
{
    ViewData["Title"]    = "All Posts";           // page title
    ViewData["Category"] = "Technology";          // auxiliary data
    return View(new PostIndexViewModel(posts));
}

// In the view:
// <title>@ViewData["Title"]</title>
// <p>Category: @ViewData["Category"]</p>

// ── ViewBag — dynamic wrapper around ViewData ─────────────────────────────
// Syntactic sugar over ViewData — same lifetime, same storage, just nicer syntax
public IActionResult Index()
{
    ViewBag.Title       = "All Posts";   // same as ViewData["Title"]
    ViewBag.PageNumber  = 1;
    ViewBag.IsAdmin     = User.IsInRole("Admin");
    return View(model);
}

// In the view: @ViewBag.Title  @ViewBag.PageNumber
// Both ViewData and ViewBag share the same underlying dictionary.
// ViewData["Title"] == ViewBag.Title — they are the same value.
Note: ViewBag is a dynamic property — there is no compile-time checking. If you mistype the property name (ViewBag.Titl instead of ViewBag.Title), the view renders an empty value with no error. ViewData is only slightly better — string key lookup still has no compile-time safety. Both are useful for small amounts of auxiliary data (page title, active navigation item, breadcrumbs) but should not be the primary way to pass data to a view. A strongly-typed ViewModel catches all property name errors at compile time.
Tip: Use ViewBag and ViewData for layout-level data that every page needs — page title (ViewBag.Title), active menu item, breadcrumbs — because this data changes per-action and cannot easily be injected into the layout. For data specific to the main content of a page, always use a strongly-typed ViewModel. A common pattern: define a BaseViewModel with properties for title, breadcrumbs, and SEO metadata, then inherit all page ViewModels from it.
Warning: TempData requires a backing store that persists across the redirect — either the session (server-side) or a cookie (client-side). TempData is stored as a cookie by default in ASP.NET Core. If cookies are disabled in the browser, TempData does not work. Cookie-backed TempData is also size-limited (4KB). For large amounts of cross-request data, use session. For sensitive data (user IDs, tokens), use session backed by Redis or a database, not cookies.

TempData — Surviving Redirects

// ── TempData survives exactly ONE redirect ─────────────────────────────────
// Set in POST action, available in the next GET action (after redirect)

// POST action — sets TempData before redirecting
[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> Create(CreatePostViewModel model)
{
    if (!ModelState.IsValid) return View(model);

    var post = await _service.CreateAsync(model.ToRequest());

    // TempData survives the redirect
    TempData["SuccessMessage"] = $"Post '{post.Title}' created successfully!";
    TempData["NewPostId"]      = post.Id;   // integer, auto-serialised

    return RedirectToAction(nameof(Index));  // redirect to GET
}

// GET action — TempData available here (cleared after this render)
public async Task<IActionResult> Index()
{
    var posts = await _service.GetPublishedAsync(1, 10);
    return View(new PostIndexViewModel(posts));
    // TempData["SuccessMessage"] is rendered in the view, then cleared
}

// ── TempData.Peek — read without clearing ─────────────────────────────────
// Normal TempData read: marked for deletion, cleared after render
string? msg = TempData["SuccessMessage"] as string;  // will be cleared

// Peek: read without marking for deletion (available in next render too)
string? msg2 = TempData.Peek("SuccessMessage") as string;  // stays for another render

// Keep: prevent TempData from being cleared after this render
TempData.Keep("SuccessMessage");  // keep for one more render

When to Use Each Mechanism

Mechanism Lifetime Use For Type-Safe?
ViewModel One render Primary page data Yes
ViewBag One render Layout data (page title) No
ViewData One render Same as ViewBag No
TempData One redirect Flash messages (PRG) No

Common Mistakes

❌ Wrong — large objects in TempData exceed the 4KB cookie limit; TempData silently fails.

✅ Correct — pass only small strings (success/error messages) in TempData; pass large data via session or re-query it in the next action.

Mistake 2 — Using ViewBag for data that should be in the ViewModel

❌ Wrong — core page data in ViewBag; typos not caught at compile time:

ViewBag.TotalPosts = await _service.CountAsync();   // magic string, easy to mistype

✅ Correct — add TotalPosts to the ViewModel where it is type-safe and checked at compile time.

🧠 Test Yourself

A POST action sets TempData["Message"] = "Saved!" and returns RedirectToAction(nameof(Index)). The Index GET action renders a view. The user refreshes the page. Is the message shown again on refresh?