All articles

Redirects in ASP.NET Core MVC: RedirectToAction, LocalRedirect and More

Choose the right redirect: RedirectToAction for app flows, 301 permanents for SEO, LocalRedirect against open redirects, and named routes with RedirectToRoute.

0 · log in to like, save & follow Share on LinkedIn Share on X
Redirects in ASP.NET Core MVC: RedirectToAction, LocalRedirect and More

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.

Redirects in ASP.NET Core MVC: RedirectToAction, LocalRedirect and More

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:

  • UseHttpsRedirection issues 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.

Enjoyed this article? Get the best GeeksArray articles in your inbox — once a week, no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.