DEV Community

Cover image for How to Pass Data Between Controllers in ASP.NET Core
Libin Tom Baby
Libin Tom Baby

Posted on

How to Pass Data Between Controllers in ASP.NET Core

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Or as a query string:

return RedirectToAction("Index", "Dashboard", new { message = "Order created", orderId = id });

[HttpGet]
public IActionResult Index(string message, Guid orderId) { }
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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}");
Enter fullscreen mode Exit fullscreen mode

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)