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.
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.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.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
Mistake 1 — Using TempData to pass large amounts of data (cookie size limit)
❌ 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.