RedirectToAction with routeValues and Route Template adds query string

57 Views Asked by At

I'm doing a Post-Redirect-Get pattern here with a couple of controllers, and I'm having trouble with the RedirectToAction part. Here is some example code with the just the redirect part to show behavior:

    public class TestController : Controller
    {
        [Route("{ID1}/Test")]
        [Route("{ID2}/{ID1}/Test")]
        public IActionResult Test(string id1, string id2)
        {
            return RedirectToAction("Redirected", new { ID1 = id1, ID2 = id2 });
        }

        [Route("{ID1}/Redirected")]
        [Route("{ID2}/{ID1}/Redirected")]
        public string Redirected()
        {
            return "Redirected";
        }
    }

If I request /a/test, I'm redirected to /a/Redirected like I expect/want to be.

If I request /a/b/test, I'm redirected to /b/Redirected?ID2=a, which is not what I expect or want!

I expect to be redirected to /a/b/Redirected.

Why can the second specified Route not be matched? (note, it seems to make no difference which order the Routes were specified)

What can I do to help/force the match I expect?

ADDENDUM:

If you use the action name as a token instead of a literal, then the expected route is selected:

        [Route("{ID1}/{action}")]
        [Route("{ID2}/{ID1}/{action}")]
        public string Redirected()
        {
            return "Redirected";
        }

Note usage of {action} instead of Redirected

This is the most relevant documentation I can find:

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-8.0#route-template-precedence-and-endpoint-selection-order

The details of how precedence works are coupled to how route templates are defined:

  • Templates with more segments are considered more specific.

  • A segment with literal text is considered more specific than a parameter segment.

  • A parameter segment with a constraint is considered more specific than one without.

  • A complex segment is considered as specific as a parameter segment with a constraint.

  • Catch-all parameters are the least specific. See catch-all in the Route templates section for important information on catch-all routes.

I'm still trying to figure out the WHY part.

1

There are 1 best solutions below

2
Md Farid Uddin Kiron On

Why can the second specified Route not be matched? (note, it seems to make no difference which order the Routes were specified)

What can I do to help/force the match I expect?

Well, based on your scenario and description, the behavior you're observing is due to the way route matching works in ASP.NET Core. When you specify multiple route templates, the routing system tries to match incoming requests to those templates in the order they are declared.

Based on your code, the unexpected query string ?ID2=a occurs because RedirectToAction tries to match route templates for the target action (Redirected) based on the route values you provide.

While you supply ID1 = b and ID2 = a, it first finds a match with the template {ID1}/Redirected, resulting in /b/Redirected. However, it then sees the additional ID2 value that doesn't fit this template, so it appends it as a query string.

In order to achieve the behavior you want, you need to be more explicit about which route template to use. You can do this by providing a specific route name to each route template, and then specifying the route name when redirecting.

You could refactor your code as following:

public class TestController : Controller
{
    [Route("{ID1}/Test", Name = "TestRoute1")]
    [Route("{ID2}/{ID1}/Test", Name = "TestRoute2")]
    public IActionResult Test(string id1, string id2)
    {
        return RedirectToAction("Redirected", new { ID1 = id1, ID2 = id2 });
    }

    [Route("{ID1}/Redirected", Name = "RedirectedRoute1")]
    [Route("{ID2}/{ID1}/Redirected", Name = "RedirectedRoute2")]
    public string Redirected()
    {
        return "Redirected";
    }
}

Now, when redirecting, specify the route name explicitly, do like below:

return RedirectToAction("Redirected", "Test", new { ID1 = id1, ID2 = id2 }).WithRouteName("RedirectedRoute1");

In addition, you also could consider attribute routing.Because, it provides clearer and more fine-grained control over route mapping.

[HttpGet("{ID2}/{ID1}/Redirected")]
public string Redirected() { ... }

Note: Please refer to this official document for atrribute and convensional routing.