Valid routes not discovered by MVC.ApiExplorer
Asked Answered
S

2

10

When using ASP.NET Web API Help Page and the related MVC.ApiExplorer I have valid routes that are accessible via http yet aren't discovered by ApiExplorer. These routes are only found when a general routing rule is used. Usage of a more specific rule (in conjunction with the general one) seems to hide routes from the ApiExplorer.

In an example case of three rules two routes relate to a GET and a POST action on a controller method which take no query parameters go MIA.

public class SomeControllerController : ApiController
{
    [HttpPost] public HttpResponseMessage Post(PostObject value) { ... }
    [HttpGet] public IEnumerable<DisplayObject> GetAll() { ... }
    [HttpGet] public DisplayObject GetById(string id) { ... }
}

When using a routing rule of

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

The routes are discovered appropriately by Api Explorer as

  • POST: api/SomeController
  • GET: api/SomeController
  • GET: api/SomeController/{id}

yet when adding the less generic and more meaningful rule

routes.MapHttpRoute(
    name: "ApiSomeControllerDefault",
    routeTemplate: "api/somecontroller/{id}",
    defaults: new
              {
                controller = "SomeController",
                id = RouteParameter.Optional
              }
    );

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

Api Explorer only returns

  • GET: api/somecontroller/{id}

What is causing some of my routes not to be found?

EDIT Link to Issue Report on ApiExplorer project page

Spent answered 24/1, 2013 at 23:44 Comment(0)
M
6

I believe what you are seeing is a known bug with ApiExplorer. What's happening is that the ApiExplorer goes through each route in the route collection and checks if the controller and its actions can be resolved.

In this case, for example, the action "GetById" can be explored by both the above routes, which ApiExplorer incorrectly assumes to be causing a conflict due to ambiguous matching and it tries to filter out duplicate actions, which in this case is causing all the actions to be filtered/removed. Since this bug is in ApiExplorer(which is part of main WebAPI core), i am afraid we cannot fix it anytime soon.

Materialize answered 25/1, 2013 at 1:51 Comment(2)
I just verified removing the "ApiDefault" rule and leaving the "ApiSomeControllerDefault" rule does indeed make the once hidden routes available to ApiExplorer.Spent
How did you remove the ApiDefault rule?Chrotoem
D
6

While this bug is not fixed by ASP.NET Web API team, I'm using my own dumb fix.

My extension method for IApiExplorer is doing the same things as original ApiDescriptions implementation in ApiExplorer class, but instead of removing duplicate actions for different routes, it just returns actions with distinct ID (HTTP method + route). So it returns all declared actions, regardless of the routes count.

And, yes, it shamelessly uses reflection to call the private method.

public static class WebApiExtensions
{
    public static Collection<ApiDescription> GetAllApiDescriptions(this IApiExplorer apiExplorer, HttpConfiguration httpConfig)
    {
        if (!(apiExplorer is ApiExplorer))
        {
            return apiExplorer.ApiDescriptions;
        }

        IList<ApiDescription> apiDescriptions = new Collection<ApiDescription>();
        var controllerSelector = httpConfig.Services.GetHttpControllerSelector();
        var controllerMappings = controllerSelector.GetControllerMapping();

        if (controllerMappings != null)
        {
            foreach (var route in httpConfig.Routes)
            {
                typeof(ApiExplorer).GetMethod("ExploreRouteControllers",
                    bindingAttr: BindingFlags.Instance | BindingFlags.NonPublic,
                    binder: null,
                    types: new[] {typeof(IDictionary<string, HttpControllerDescriptor>), typeof(IHttpRoute), typeof(Collection<ApiDescription>)},
                    modifiers: null
                ).Invoke(apiExplorer, new object[] {controllerMappings, route, apiDescriptions});
            }

            apiDescriptions = apiDescriptions
                .GroupBy(api => api.ID.ToLower())
                .Select(g => g.First())
                .ToList();
        }

        return new Collection<ApiDescription>(apiDescriptions);
    }
}

It's easy to use:

var apiDescriptions = apiExplorer.GetAllApiDescriptions(httpConfig);

HttpConfiguration parameter added for testability. If you don't care about it, remove the parameter and just use GlobalConfiguration.HttpConfiguration in the extension method directly.

Drynurse answered 17/3, 2013 at 19:28 Comment(2)
Thank you so much. This answer solved my problem after so much fighting with the framework!!!Whisenhunt
Almost 10 years later and this helped immensely when fixing a bug in a legacy application.Filing

© 2022 - 2024 — McMap. All rights reserved.