In web development, you frequently need to send users somewhere else — after a form submission, after login, or when a page has moved permanently. ASP.NET Core provides multiple redirect mechanisms, each serving a specific HTTP purpose. Understanding when to use each is essential for building production-grade applications.
Redirect results are ActionResult types that instruct the browser to make a new HTTP request to a different URL. Instead of returning HTML content, the server sends a 3xx HTTP status code with a Location header, causing the browser to automatically navigate to the new URL.
| Method | HTTP Code | Type | Use Case |
|---|---|---|---|
RedirectToAction() | 302 | Temporary | Navigate to another action in your app (most common) |
RedirectToActionPermanent() | 301 | Permanent | SEO migration — old action moved permanently |
RedirectToRoute() | 302 | Temporary | Navigate using route names instead of action names |
Redirect() | 302 | Temporary | Navigate to an external URL or absolute path |
RedirectPermanent() | 301 | Permanent | Old URL permanently moved (SEO) |
RedirectPreserveMethod() | 307 | Temporary | Redirect but keep the original HTTP method (POST stays POST) |
RedirectPermanentPreserveMethod() | 308 | Permanent | Permanent redirect, preserve HTTP method |
LocalRedirect() | 302 | Temporary | Safe redirect — only allows local URLs (prevents open redirect attacks) |
public class OrdersController : Controller
{
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> PlaceOrder(OrderViewModel model)
{
if (!ModelState.IsValid)
return View("Checkout", model); // Return view (no redirect)
var orderId = await _orderService.CreateAsync(model);
TempData["Success"] = $"Order #{orderId} placed!";
// RedirectToAction → 302 → Browser makes new GET request
return RedirectToAction(nameof(Confirmation), new { id = orderId });
}
public IActionResult Confirmation(int id)
{
// This is a GET — user can refresh safely without re-submitting
var order = _orderService.GetById(id);
return View(order);
}
}
[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid) return View(model);
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, false, false);
if (result.Succeeded)
{
// SECURITY: Use LocalRedirect instead of Redirect
// This prevents attackers from crafting URLs like: /login?returnUrl=https://evil-site.com
if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
return LocalRedirect(returnUrl); // Only allows URLs on YOUR domain
return RedirectToAction("Index", "Dashboard");
}
ModelState.AddModelError("", "Invalid login attempt");
return View(model);
}
// Old URL structure: /products/view/42
// New URL structure: /shop/42
// 301 tells search engines to update their index permanently
[Route("products/view/{id:int}")]
public IActionResult OldProductPage(int id)
{
// 301 Permanent Redirect — SEO juice transfers to new URL
return RedirectToActionPermanent("Details", "Shop", new { id });
}
// External URL redirect (old domain to new domain)
[Route("legacy-page")]
public IActionResult LegacyPage()
{
return RedirectPermanent("https://newdomain.com/updated-page");
}
// Define a named route
app.MapControllerRoute(
name: "productDetail",
pattern: "shop/{category}/{slug}",
defaults: new { controller = "Shop", action = "Product" }
);
// Redirect using route name (decoupled from controller/action names)
public IActionResult GoToProduct()
{
return RedirectToRoute("productDetail", new { category = "electronics", slug = "iphone-15" });
// Generates: /shop/electronics/iphone-15
}
Never use Redirect(returnUrl) with user-supplied URLs without validation. Attackers can craft login links like /login?returnUrl=https://phishing-site.com. Always use LocalRedirect() or validate with Url.IsLocalUrl(returnUrl). This is a real OWASP Top 10 vulnerability that has been exploited in major applications.
nameof() instead of magic strings: RedirectToAction(nameof(Index))LocalRedirect() for user-supplied return URLs to prevent open redirect attacksQ: "What is the Post-Redirect-Get pattern and why is it important?"
Architect Answer: "PRG is a web design pattern that prevents duplicate form submissions. After processing a POST request (e.g., placing an order), the server responds with a 302 redirect to a GET endpoint (e.g., order confirmation). If the user hits refresh, the browser re-sends the GET request — not the POST. Without PRG, refreshing after a POST would re-submit the form, potentially creating duplicate orders, double charges, or duplicate database records. In every production application I've built, POST handlers always end with RedirectToAction(), never View()."