How to modify ASP NET Web API Route matching to al

2019-07-15 13:21发布

We're using RavenDB on the backend and so all the DB keys are strings that contain forward slashes e.g. users/1.

AFAIK there's no way to use the default ASP NET Web API route matching engine to match a parameter like this, without using a catch-all which means the key must be the last thing on the URL. I tried adding a regex constraint of users/d+ but it didn't seem to make a difference, the route wasn't matched.

What bits would I have to replace to do just enough custom route matching to allow this, preferably without crippling the rest of the route matching. For example, using url: "{*route}" and a custom constraint that did full regex matching would work but may work unexpectedly with other route configurations.

If your answer comes with some sample code, so much the better.

1条回答
孤傲高冷的网名
2楼-- · 2019-07-15 13:33

It seems that it is possible to do this by defining a custom route. In MVC4 (last stable released 4.0.30506.0), it is not possible to do by implementing IHttpRoute as per specification but by defining a custom MVC-level Route and adding it directly to the RouteTable. For details see 1, 2. The RegexRoute implementation below is based on the implementation here with mods from the answer here.

Define RegexRoute:

public class RegexRoute : Route
{
    private readonly Regex urlRegex;

    public RegexRoute(string urlPattern, string routeTemplate, object defaults, object constraints = null)
        : base(routeTemplate, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), new RouteValueDictionary(), HttpControllerRouteHandler.Instance)
    {
        urlRegex = new Regex(urlPattern, RegexOptions.Compiled);
    }

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        string requestUrl = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;

        Match match = urlRegex.Match(requestUrl);

        RouteData data = null;

        if (match.Success)
        {
            data = new RouteData(this, RouteHandler);

            // add defaults first
            if (null != Defaults)
            {
                foreach (var def in Defaults)
                {
                    data.Values[def.Key] = def.Value;
                }
            }

            // iterate matching groups
            for (int i = 1; i < match.Groups.Count; i++)
            {
                Group group = match.Groups[i];

                if (group.Success)
                {
                    string key = urlRegex.GroupNameFromNumber(i);

                    if (!String.IsNullOrEmpty(key) && !Char.IsNumber(key, 0)) // only consider named groups
                    {
                        data.Values[key] = group.Value;
                    }
                }
            }
        }

        return data;
    }
}

Add this DelegatingHandler to avoid a NullReference due to some other bug:

public class RouteByPassingHandler : DelegatingHandler
    {
        protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            HttpMessageInvoker invoker = new HttpMessageInvoker(new HttpControllerDispatcher(request.GetConfiguration()));
            return invoker.SendAsync(request, cancellationToken);
        }
    }

Add handler and route directly to the RouteTable:

RouteTable.Routes.Add(new RegexRoute(@"^api/home/index/(?<id>\d+)$", "test", new { controller = "Home", action = "Index" }));

        config.MessageHandlers.Add(new RouteByPassingHandler());

Et voila!

EDIT: This solution has problems when the API is self-hosted (instead of using a WebHost) and requires further work to make it work with both. If this is interesting to anyone, please leave a comment and I'll post my solution.

查看更多
登录 后发表回答