TempData, Session, RouteData, RedirectToAction with params, service-layer approach (the right way)
It's a common scenario: one controller handles a form submission, and you need to pass a message or result to another controller's action after a redirect.
But passing data between controllers is trickier than it looks — and most approaches have hidden tradeoffs.
Why You Can't Just Use a Variable
HTTP is stateless.
A redirect from OrderController to DashboardController is a completely new HTTP request. Any variables you set in the first request are gone by the time the second request arrives.
You need a mechanism to persist data across that boundary.
Option 1: TempData
TempData is a dictionary that persists for exactly one redirect — cleared after the next request reads it.
// OrderController.cs
[HttpPost]
public IActionResult Create(CreateOrderDto dto)
{
var orderId = _orderService.Create(dto);
TempData["SuccessMessage"] = "Order created successfully!";
TempData["OrderId"] = orderId.ToString();
return RedirectToAction("Index", "Dashboard");
}
// DashboardController.cs
[HttpGet]
public IActionResult Index()
{
var message = TempData["SuccessMessage"] as string;
ViewBag.Message = message;
return View();
}
When to use
Simple success/error messages after a redirect.
Limitations
- Only survives one redirect
- Stores only simple types (strings, ints)
- Requires session or cookie configuration
// Required in Program.cs
builder.Services.AddSession();
app.UseSession();
Option 2: Route Parameters and Query Strings
Pass data in the URL itself.
// Pass as route parameter
return RedirectToAction("Confirm", "Orders", new { id = orderId });
[HttpGet("orders/confirm/{id}")]
public IActionResult Confirm(Guid id)
{
var order = _orderService.GetById(id);
return View(order);
}
Or as a query string:
return RedirectToAction("Index", "Dashboard", new { message = "Order created", orderId = id });
[HttpGet]
public IActionResult Index(string message, Guid orderId) { }
When to use
IDs, filters, page numbers — anything safe to expose in the URL and should be bookmarkable.
Limitations
- Visible in the URL — do not pass sensitive data
- Limited to simple types
- Long query strings can hit browser URL length limits
Option 3: The Service Layer — The Recommended Approach
In well-architected ASP.NET Core applications, controllers should not communicate with each other at all.
They should both call the same service or query the same data.
// ❌ Anti-pattern: passing data controller-to-controller
// ✅ Correct: both controllers use the same service
public class OrderController : ControllerBase
{
private readonly IOrderService _orderService;
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateOrderDto dto)
{
var orderId = await _orderService.CreateAsync(dto);
return Ok(new { orderId });
}
}
public class DashboardController : ControllerBase
{
private readonly IOrderService _orderService;
[HttpGet]
public async Task<IActionResult> Index()
{
var recentOrders = await _orderService.GetRecentAsync();
return Ok(recentOrders);
}
}
Each controller gets what it needs from the service layer. No controller-to-controller dependency. No state passed between them.
Option 4: Session
Session stores data server-side, keyed to a session cookie.
// Store
HttpContext.Session.SetString("LastOrderId", orderId.ToString());
HttpContext.Session.SetString("UserMessage", "Order created");
// Retrieve
var lastOrderId = HttpContext.Session.GetString("LastOrderId");
var message = HttpContext.Session.GetString("UserMessage");
When to use
Multi-step wizards (shopping carts, multi-page forms) where state must persist across several requests.
Limitations
- Stateful — complicates horizontal scaling (use Redis for distributed session)
- Must manage session expiry and cleanup
- Not suitable for REST APIs
Option 5: IMemoryCache
Cache data by a key and read it in the next action.
// Store after creating order
_cache.Set($"order-result-{userId}", new OrderResult(orderId, "Created"),
TimeSpan.FromMinutes(5));
// Read in next action
var result = _cache.Get<OrderResult>($"order-result-{userId}");
When to use
When you need to pass richer objects across a redirect and TempData is too limited.
Comparison at a Glance
| Approach | Survives | Stores | Use for |
|---|---|---|---|
| TempData | 1 redirect | Simple types | Flash messages |
| Query string | Permanent (URL) | Simple types | IDs, filters |
| Service layer | N/A — preferred | Anything | Everything |
| Session | Duration of session | Simple types | Wizards, carts |
| IMemoryCache | Cache TTL | Any type | Rich transient state |
Interview-Ready Summary
-
TempData= persists for exactly one redirect — good for flash messages - Query strings and route params = safe for IDs and filters, visible in URL
- The service layer = the correct architectural answer — controllers share services, not state
- Session = stateful, requires configuration, suited for multi-step flows
- In REST APIs, controllers should be stateless — never pass state between them
A strong interview answer:
"In ASP.NET Core, TempData handles simple messages across a single redirect. Route and query parameters work for IDs and filters. But the correct architectural answer is the service layer — both controllers call the same service to get what they need, and no state is passed between controllers at all. In REST APIs, controllers should be stateless, with all shared state living in the database or cache, not in controller-level variables."
Top comments (0)