Redirects are one-line calls that hide four distinct decisions: where to send the user, which HTTP status to use, whether search engines should update their index, and whether the target can be trusted. ASP.NET Core MVC gives you a family of helpers — RedirectToAction, Redirect, LocalRedirect, RedirectToRoute, and their Permanent variants — and choosing the right one matters for both UX and SEO.

Every example below returns a real response captured from the companion .NET 10 project.
The redirect family at a glance
| Helper | Status | Target | Use when |
|---|---|---|---|
RedirectToAction |
302 | controller/action | normal in-app navigation |
RedirectToActionPermanent |
301 | controller/action | URL moved for good |
Redirect(url) |
302 | any URL | external sites |
LocalRedirect(url) |
302 | local only — throws otherwise | returnUrl round-trips |
RedirectToRoute |
302 | named route | route-first URL design |
RedirectToAction — the everyday one
public class RedirectDemoController : Controller
{
public IActionResult ToAction() => RedirectToAction("Index", "Products");
}
GET /RedirectDemo/ToAction
HTTP 302 → Location: /Products
This is the backbone of the Post/Redirect/Get pattern: after a successful form POST, redirect to a GET so a browser refresh cannot resubmit the form. Pass route values as an anonymous object: RedirectToAction("Detail", "Products", new { id = 2 }).
A 302 tells browsers and crawlers "temporarily elsewhere — keep using the original URL." That is correct for form flows and wrong for moved content.
Permanent redirects and SEO
When a URL changes for good — a renamed slug, a retired page with a successor — use the Permanent variant so search engines transfer the old URL's ranking to the new one:
public IActionResult OldPage() => RedirectToActionPermanent("Index", "Products");
GET /RedirectDemo/OldPage
HTTP 301 → Location: /Products
We use exactly this on geeksarray.com: dozens of legacy article URLs 301 to their modernized replacements, which preserved years of search ranking through a full platform rewrite. The rule of thumb: 302 while you might change your mind, 301 once you won't — browsers cache 301s aggressively, so a wrong permanent redirect is painful to undo.
Redirect — arbitrary and external URLs
public IActionResult External() => Redirect("https://geeksarray.com/blog");
Redirect sends the browser wherever you say — including off your site. That power is the problem: if the URL comes from user input, you have built an open redirect, a phishing vector where https://yoursite/login?returnUrl=https://evil.example bounces victims from your trusted domain to an attacker's.
LocalRedirect — the safe returnUrl helper
public IActionResult Local(string? returnUrl) => LocalRedirect(returnUrl ?? "/");
LocalRedirect throws InvalidOperationException for anything that isn't a local path, killing the open-redirect class outright. Every login flow that round-trips a returnUrl should finish with LocalRedirect (or an explicit Url.IsLocalUrl check). If you genuinely need to send users to a vetted external site, compare against an allowlist first and then use Redirect.
RedirectToRoute — when routes are the contract
If you name your routes, you can redirect to the route rather than a controller/action pair:
[HttpGet("/products/{id:int}", Name = "product-detail")]
public IActionResult Detail(int id) { /* ... */ }
public IActionResult ByRoute() => RedirectToRoute("product-detail", new { id = 2 });
GET /RedirectDemo/ByRoute
HTTP 302 → Location: /products/2
This decouples callers from controller names — refactor the controller freely and the named route keeps working. The same names feed Url.RouteUrl for link generation.
Redirecting outside MVC
Two related tools live at the pipeline level rather than in controllers:
UseHttpsRedirectionissues 307/308 redirects from HTTP to HTTPS for the whole app.- Custom middleware suits cross-cutting URL policies. On geeksarray.com a small middleware 301s any request that arrives via the server's raw IP to the canonical domain, so relative links never leak the IP into crawlers:
if (canonicalHost is { Length: > 0 } &&
IPAddress.TryParse(context.Request.Host.Host, out _))
{
context.Response.Redirect($"https://{canonicalHost}{context.Request.Path}", permanent: true);
return;
}
For bulk legacy-URL maps (hundreds of moved pages), a dictionary-driven middleware beats scattering RedirectPermanent calls across controllers.
Status codes, precisely
- 301 permanent, method may change to GET. 308 permanent, method preserved.
- 302 temporary, method may change to GET. 307 temporary, method preserved.
MVC's helpers produce 301/302; use RedirectPreserveMethod / RedirectToActionPreserveMethod when a POST must stay a POST across the hop (307/308) — rare, but it exists for API-style flows.
Summary
Reach for RedirectToAction for in-app flows and Post/Redirect/Get, the Permanent variants when a URL has genuinely moved (and you want SEO to follow), LocalRedirect for anything involving a user-supplied return URL, RedirectToRoute when named routes are your stable contract, and middleware when the rule applies to every request. The companion repository contains all five variants with the exact responses shown above.
Comments (0)
No comments yet — be the first to share your thoughts.