steve-jansen / Swagger.Net

Library to document the ASP.NET Web API using the Swagger specification
0 stars 3 forks source link

Support routing rules with custom route templates #6

Open steve-jansen opened 11 years ago

steve-jansen commented 11 years ago

Overview Swagger.Net gets confused with WebAPI routes using action names that are not standard HTTP verbs. This is necessary to route the same HTTP verb, like GET, to multiple controller actions.

This is a corrolary of issue #5.

Steps To Reproduce

  1. Start the Swagger.Net.WebAPI project in VS with commit 6d4093f088 or later
  2. Navigate to http://localhost:####/api/docs/Pet

Actual Results Routes to controller actions using custom route templates fall back to using the default API route, like PUT /api/Pet/PutExport/{id}?userId={userId} instead of the custom PUT /api/Pet/{id}/User/{userId}

Expected Results Swagger should document the custom route only PUT /api/Pet/{id}/User/{userId}

Example Routing Config configuration.Routes.MapHttpRoute( name: "Custom route for linking a pet to a user", routeTemplate: "api/Pet/{id}/User/{userId}", defaults: new { controller="Pet", action="PutUser", userId = RouteParameter.Optional }, constraints: new { id = @"\d+", userId = @"\d*" });

Screenshot image

steve-jansen commented 11 years ago

I noticed that we might want to override XmlDocProvider::GetDocumentation to return the fully qualified member name instead of the summary documentation to ensure a unique name that is consistent across all route variations. Not sure if this is a good idea or not.

steve-jansen commented 11 years ago

I also think we can be opinionated that the route template with the fewest querystring parameters is the most specific (and best) route template to use.

steve-jansen commented 11 years ago

We can also iterate through the Collection<ApiDescription> to find all duplicates within the ApiDocumentation::Documentation member. We should prefer ApiDescription::Route values that don't match the default route.

Alternatively, we can use the first match in GlobalConfiguration.Configuration.Routes, since the WebAPI runtime will use the first match found in this collection. So, use the first route that matches, based on priority. Don't use the lower priority routes as the RelativePath.

steve-jansen commented 11 years ago

Another final option is instead of using ActionDescription::RelativePath as the category name, we could use the Controller Name on the ActionDescription::ControllerDescription::ControllerName value. Or, parse the route tempate and stop at the first {} value after replacing the {controller} and {action} params and prepending ActionDescription::SupportedHttpMethods

mknayak commented 11 years ago

I guess this behavior is due to the APIExplorer.ApiDescriptions() and I think it is working perfectly. It will try to get all possible route combinations based on the routes defined. Here we have declared 3 api routes,

 configuration.Routes.MapHttpRoute(
                name: "Custom route for linking a pet to a user",
                routeTemplate: "api/Pet/{id}/User/{userId}",
                defaults: new { controller = "Pet", action = "PutUser", userId = RouteParameter.Optional },
                constraints: new { id = @"\d+", userId = @"\d*" });

            configuration.Routes.MapHttpRoute(
                name: "Controller Actions That Are Not HTTP Verbs",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional },
                constraints: new { action = @"\D+", id = @"\d*", });

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

If we consider the route for action.

[HttpPut]
[ActionName("PutExport")]
public HttpResponseMessage Put(int id, int? userId = null)
{
    return Request.CreateResponse(HttpStatusCode.NoContent);
}

it can be reached by below combination,

api/Pet/PutExport/{id}?userId={userId}     // Route 2
api/Pet/{id}?user={userId}     // Route 3

In Route it is defined as PutUser but action name is PutExport. It we make them in sync, by changing either, it will start matching 1st route, so valid routes will be

api/Pet/{id}/User/{userId}   // Route 1
api/Pet/PutUser/{id}?userId={userId}   // Route 2
api/Pet/{id}?userId={userId}  // Route 3

capture