10

An existing project has controllers that inherit from either:

  • Controller: RouteTable.Routes.MapRoute with "{controller}/{action}/{id}".
  • ApiController: GlobalConfiguration.Configure and in the callback MapRoute with "api/{controller}/{id}".

Everything works fine, but I need to generate URLs for action methods in both of these types of controllers. Given:

  1. a name or type of a controller that inherits from either of these, and
  2. an action method name

Then from the web site side, how can I generate proper URLs for the web API side?

I'm using reflection to get action and controller names right now, and then through using UrlHelper.Action(actionName, controllerName, routeValueDictionary) am getting the correct URL for web site routes.

However, this method is (of course) generating URLs like this for the WebAPI side: /ApiControllerName/Get?parameter1=value when it needs to be /api/ApiControllerName?parameter1=value and the separate knowledge that it's a GET request.

Purpose: this is for a smoke test page for the web site that uses attributes and reflection to decide what to smoke test. It would be nice to be able to use the same attribute throughout the project, and furthermore it would be very nice to be able to use the correct UrlHelper that is aware of the routing tables and can produce the right prefix such as /api/, instead of the code assuming, perhaps wrongly, that the API routes were registered with api and not, say, webapi.

Update

After continued research I have found the Url.HttpRouteUrl method which can generate WebAPI URLs, but this requires knowing a route name, not an action method name.

I've done even more research on this and haven't gotten any closer to a solution. It looks like if you know the route name of the appropriate route, you can cook up a Url easily. There are also some likely hints here and here. But if there are multiple routes for WebApi, how do you know which one matches the controller and action you want? It would be dumb to reimplement what MVC itself already does in selecting a controller and action. I guess I could construct a URL from the given parameters using every WebApi route, then run the URL through its paces (using some of the above links) and see if it is going to match the desired controller... yuck.

There's got to be an easier way.

For now I'm going to have to move on, but here's to hoping someone can help me out.

Community
  • 1
  • 1
ErikE
  • 48,881
  • 23
  • 151
  • 196

3 Answers3

5

Here a few ways of do it:

  • Using RouteUrl() method:

    var url1 = Url.RouteUrl(new { id = 1, controller = "...",  httproute = true });
    

The trick is of course httproute = true. Setting this property, you inform that you only want http routes (Web Api routes).

  • Using HttpRouteUrl() method:

    var url2 = Url.HttpRouteUrl(null, new { id = 2, controller = ".." });
    
  • And another way to do it using directly the routes and httproute value:

    var values = new RouteValueDictionary(new
    {
        id = 3,
        controller = "...",
        httproute = true
    });
    var url3 = RouteTable.Routes.GetVirtualPath(Request.RequestContext, values).VirtualPath;
    

The 3 ways are basically the same. Since you don't specify the route, the system will try to find the first match according to the route values. For example, lets say you have an ApiController called RestfulController and web api routes are configured like so:

config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

Using the first method, when you do var url = Url.RouteUrl(new { id = 123, controller = "restful", httproute = true }); in a MVC Controller, the value in url is /api/restful/123.

But if you add a new route ConstraintApi:

config.Routes.MapHttpRoute(
    name: "ConstraintApi",
    routeTemplate: "api2/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional },
    constraints: new { controller = "restful" }
);

config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

The url returned by RouteUrl is /api2/restful/123.

You should be aware the order when you are declaring your routes. If the route DefaultApi is added before the route ConstraintApi, the url generated is /api/restful/123.

Arturo Menchaca
  • 15,783
  • 1
  • 29
  • 53
  • It works, with a little clarification. for `httproute = false`, one must use `UriHelper.Action` and pass in the action method name as a parameter, not put it in the `RouteValuesCollection`. Also, in WebApi, there's some funniness about figuring out the HTTP verb correctly, but for now I'm satisfied (passing in `action = "Get"` or `action = "MethodName"` just projects a querystring value). – ErikE Mar 20 '16 at 00:24
3

There is the Hyprlinkr library which allows you to create URIs from the Controller and Action using the route configuration.

ChrisS
  • 376
  • 1
  • 11
  • That looks interesting and may turn out to be just what I need, but in the meantime it's not ready for me out of the box because "it requires an instance of the `HttpRequestMessage` class, which is provided by the ASP.NET Web API for each request. The preferred way to get this instance is to implement a custom `IHttpControllerActivator` and create the RouteLinker instance from there." This means I'll have to do something clever to get it working in the web side (which of course does not readily have an instance of `HttpRequestMessage`). – ErikE Mar 10 '16 at 16:29
0

This seems more like a hack, but worth a try if you only plan to use it for testing. As you mentioned, you can generate a URL with a route name. So you could grab all route names and generate all possible URLs. Then you need to check if the URL resolves to the action that you need or not. For that you can use something like the following: https://stackoverflow.com/a/19382567/1410281. The basic idea is that if you've got a URL, then do a fake redirect there, and get the routing data from the framework.

Community
  • 1
  • 1
Tamas
  • 6,260
  • 19
  • 30
  • That's an interesting idea. How would parameters play into it—that is, when action methods only differ by parameter list, how do I reliably generate a URL that will hit the desired action method? Also, what if the routes were set up without route names? – ErikE Mar 15 '16 at 14:15
  • As I said it's not a full fledged solution but rather a hack. So parameters would work as far as you can go with reflection and it will most probably work only for the cases that you need. (You mentioned in your update, that it's possible to generate the Url if you have the route name, so I simply considered that you can solve that) – Tamas Mar 15 '16 at 17:06
  • As for routes not having names, you could create your own `Route` and `RouteCollection` class, that holds the route name, or when nothing is specified than it generates one that you can refer to. – Tamas Mar 15 '16 at 17:13
  • Wow, so iterate over the current RouteCollection, create a new RouteCollection with artificially assigned names (for any routes missing them), then use this to generate a URL that matches each combination within every route, then check and see if that URL actually resolves to the desired controller-actions. How about route constraints like a regular expression looking for only numbers, how would I know how to create a route that has an integer parameter without basically reverse-engineering MVC's route dispatching? – ErikE Mar 15 '16 at 17:23