Tutorials ASP.NET Core MVC Mastery
RedirectResult
On this page
RedirectResult in ASP.NET Core MVC — Mastering Navigation & PRG Pattern
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.
1. WHAT Are Redirect Results?
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.
2. The Complete Redirect Family
| 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) |
3. REAL-TIME PRODUCTION EXAMPLES
Example 1: Post-Redirect-Get (PRG) — The Most Common Pattern
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);
}
}
Example 2: Safe Redirect After Login (Preventing Open Redirect Attacks)
[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);
}
Example 3: SEO-Friendly Permanent Redirects (URL Migration)
// 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");
}
Example 4: Redirect to Named Routes
// 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
}
4. 301 vs 302 — The Critical SEO Distinction
302 — Temporary Redirect
- Browser requests the new URL this time only
- Search engines keep the old URL in their index
- Use for: PRG pattern, login flows, A/B testing
301 — Permanent Redirect
- Browser caches the redirect and never requests the old URL again
- Search engines transfer all SEO ranking to the new URL
- Use for: URL restructuring, domain migration, slug changes
Security: Open Redirect Vulnerability
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.
5. Best Practices
- Always use PRG for form submissions to prevent duplicate submissions on browser refresh
- Use
nameof()instead of magic strings:RedirectToAction(nameof(Index)) - Use
LocalRedirect()for user-supplied return URLs to prevent open redirect attacks - Use 301 only when you're certain the URL has permanently moved — browsers cache it aggressively
- Combine with TempData to pass notification messages across the redirect boundary
6. Interview Mastery
Q: "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()."