3

I'm looking for a route definition that is able to match routes like:

  • /segment/xxxx/def
  • /segment/.../xxxx/def
  • /segment/that/can/span/xxxx/def

and be able to run the action xxxx with param `def.

But this kind of route isn't allowed:

[Route("/{*segment}/xxx/{myparam}")]

How can it be done?

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
Tim
  • 1,749
  • 3
  • 24
  • 48
  • catch-all placeholders cannot be anywhere other than the end of the route template. – Nkosi Feb 27 '18 at 15:36
  • Reference [Routing in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing) Reference [Routing to Controller Actions](https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing) – Nkosi Feb 27 '18 at 15:38
  • ok, but how to do that ? – Tim Feb 27 '18 at 15:48
  • Nkosi just said you _can't_ do that – ADyson Feb 27 '18 at 16:19
  • no other option ? custom routing with IRouter ? url rewriting ? regex ? i don't know – Tim Feb 27 '18 at 16:33

1 Answers1

5

You can use a custom IRouter combined with a regular expression to do advanced URL matching such as this.

public class EndsWithRoute : IRouter
{
    private readonly Regex urlPattern;
    private readonly string controllerName;
    private readonly string actionName;
    private readonly string parameterName;
    private readonly IRouter handler;

    public EndsWithRoute(string controllerName, string actionName, string parameterName, IRouter handler)
    {
        if (string.IsNullOrWhiteSpace(controllerName))
            throw new ArgumentException($"'{nameof(controllerName)}' is required.");
        if (string.IsNullOrWhiteSpace(actionName))
            throw new ArgumentException($"'{nameof(actionName)}' is required.");
        if (string.IsNullOrWhiteSpace(parameterName))
            throw new ArgumentException($"'{nameof(parameterName)}' is required.");
        this.controllerName = controllerName;
        this.actionName = actionName;
        this.parameterName = parameterName;
        this.handler = handler ??
            throw new ArgumentNullException(nameof(handler));
        this.urlPattern = new Regex($"{actionName}/[^/]+/?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
    }

    public VirtualPathData GetVirtualPath(VirtualPathContext context)
    {
        var controller = context.Values.GetValueOrDefault("controller") as string;
        var action = context.Values.GetValueOrDefault("action") as string;
        var param = context.Values.GetValueOrDefault(parameterName) as string;

        if (controller == controllerName && action == actionName && !string.IsNullOrEmpty(param))
        {
            return new VirtualPathData(this, $"{actionName}/{param}".ToLowerInvariant());
        }
        return null;
    }

    public async Task RouteAsync(RouteContext context)
    {
        var path = context.HttpContext.Request.Path.ToString();

        // Check if the URL pattern matches
        if (!urlPattern.IsMatch(path, 1))
            return;

        // Get the value of the last segment
        var param = path.Split('/').Last();

        //Invoke MVC controller/action
        var routeData = context.RouteData;

        routeData.Values["controller"] = controllerName;
        routeData.Values["action"] = actionName;
        // Putting the myParam value into route values makes it
        // available to the model binder and to action method parameters.
        routeData.Values[parameterName] = param;

        await handler.RouteAsync(context);
    }
}

Usage

app.UseMvc(routes =>
{
    routes.Routes.Add(new EndsWithRoute(
        controllerName: "Home", 
        actionName: "About", 
        parameterName: "myParam", 
        handler: routes.DefaultHandler));

    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

This route is parameterized to allow you to pass in the controller, action, and parameter names that correspond to the action method being called.

public class HomeController : Controller
{
    public IActionResult About(string myParam)
    {
        ViewData["Message"] = "Your application description page.";

        return View();
    }
}

It would take some more work to make it match any action method name and be able to build the URL again with that action method name. But this route will allow you to add additional action names by registering it more than one time.

NOTE: For SEO purposes, it is generally not considered a good practice to put the same content on multiple URLs. If you do this, it is recommended to use a canonical tag to inform the search engines which of the URLs is the authoritative one.

See this to accomplish the same in ASP.NET MVC (prior to ASP.NET Core).

NightOwl888
  • 55,572
  • 24
  • 139
  • 212