Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error: "Not supported by Swagger 2.0: Multiple operations with path 'api/Products' and method 'GET'" #142

Closed
ChristianWeyer opened this issue Dec 10, 2014 · 34 comments

Comments

@ChristianWeyer
Copy link

@ChristianWeyer ChristianWeyer commented Dec 10, 2014

Hm... I see in the README that Swagger 2.0 does not include the query string component when mapping a URL to an action.
As a result, Swashbuckle will raise an exception if it encounters multiple actions with the same path (sans query string) and HTTP method.

Phew. Is there any generic solution for that? We (and others, I figure) do have a lot of APIs with query strings and a standard route like
config.Routes.MapHttpRoute("DefaultApi", "api/{controller}");

Any ideas?
Thanks!

@domaindrivendev
Copy link
Owner

@domaindrivendev domaindrivendev commented Dec 10, 2014

Unfortunately this is a constraint imposed by the Swagger specification https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md.

I had some late involvement with the Swagger 2.0 working group and pushed hard to have this constraint removed but to no avail. As you pointed out, this does cause an issue for certain WebApi implementations. I'll describe the workaround below but first I want to play devil's advocate and look at it from an API perspective, agnostic of implementation frameworks or even specific language constructs.

In it's essence, the constraint really just says the following paths have to be described as one operation.

GET api/products
GET api/products?producType={productType}

Behind the scenes these may be implemented as separate C# or separate Java methods, but in the context of describing a REST API, would you describe them separately? Swagger 2.0 says "No ... it's one operation with an optional productType parameter".

Technically, I believe they are two different resources (hence why I opposed the constraint) but I do see some sense in the Swagger 2.0 approach. For example, I don't think I've ever seen any API docs broken down this way - it's invariably just by path with additional information about query parameters included.

Anyway, philosophy aside - breaking the Swagger 2.0 spec is not an option and so I can only provide some workarounds.

The most straight forward would be to consolidate your multiple, "overloaded", actions with a single action with optional parameters. You could even delegate internally to the private overloaded versions.

If a change to the implementation isn't an option - Swashbuckle 5.0 provides a config setting ResolveConflictingActions. This takes a function of the form - Func<<IEnumerable<ApiDescription>, ApiDescription> which you can provide to consolidate the actions into one ApiDescription, and therefore one Operation, at the documentation level only.

This "merge" process will get trickier if the response type and errors codes also differ but at that point you'd have to ask questions about the API design.

Hope this helps - let me know if it makes sense?

@ChristianWeyer
Copy link
Author

@ChristianWeyer ChristianWeyer commented Dec 10, 2014

Hmm...
I think having a method for list of products like this:
public IEnumerable GetProducts()

and one method for getting product details like this:
public ProductDto GetProduct(int id)

is a very common scenario.
Different return types, at least.

So yes: changing the implementation is not an option.
Hm...

@domaindrivendev
Copy link
Owner

@domaindrivendev domaindrivendev commented Dec 10, 2014

True, it's extremely common to have a "collection" resource and corresponding "item" resource. However, the standard approach here would be to use a "path" parameter for the id:

GET /api/products
GET /api/products/{id}

If you're early in your API design, I would strongly advice this approach. It's a widely accepted standard in REST design. In fact it's reflected by the default route template when you create a new WebApi project:

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

Swagger will have no problem with this because the two different operations can be distinguished by path alone. If you want to go against the grain and represent the two different operations as follows:

GET /api/products
GET /api/products?id={id}

Then you will find it very difficult to describe your API with Swagger. Although it does represent best practice, I do think Swagger 2.0 is a little too opinionated in this regard. Swashbuckle provides good workarounds and frankly, can't do much more. If you feel very strongly about this, I would recommend posting an issue here - https://github.com/swagger-api/swagger-spec

@OnamChilwan
Copy link

@OnamChilwan OnamChilwan commented Mar 4, 2015

@domaindrivendev you mention workarounds via Swashbuckle what are these? The only one I have comes across is using the first action in the list which hides implementation in my opinion and isn't ideal. Is there a way of exposing your example above?

@domaindrivendev
Copy link
Owner

@domaindrivendev domaindrivendev commented Mar 4, 2015

True, it's not ideal! However, I still maintain that it's due to an overly opinionated constraint in the Swagger specification and with that being the root cause, maybe worth posting an issue there.

The bottom line is this ... because of this constraint, you CANNOT describe the actions as separate Swagger Operations and so, the corresponding ApiDescriptions have to be merged into one for the Swagger document to be generated. This is what the ResolveConflictingActions option is for.

It's also worth noting that you're not limited to just taking the first description, you're free to implement any merge strategy you like. For example, if the actions share the same response type and only differ in parameters, you could return a completely new ApiDescription that includes a union of parameters, marking the non-common ones as optional.

Finally, if they don't share the same response type ... then, at an API level (independent of SB, C# or any server-side frameworks) you simply won't be able to describe your API with Swagger.

@decoder318
Copy link

@decoder318 decoder318 commented Jun 9, 2015

@domaindrivendev

The workaround specified in the documentation won't work.

httpConfiguration
.EnableSwagger((c) =>
{
c.SingleApiVersion("v1", "A title for your API"));
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
});

cuz apiDescription is of type IEnumerable and not an ICollection or IList
Is there a better way of handling this? Or did you forget updating the documentation?

@decoder318
Copy link

@decoder318 decoder318 commented Jun 9, 2015

Also, for some reason, the rendered swagger lists about 30 controllers, and ignores the rest.

@jbongaarts
Copy link

@jbongaarts jbongaarts commented Jun 24, 2015

@decoder318 The work around does work. First() is an extension method for IEnumerable. Add 'using System.Linq;' to make it available.

@gjj
Copy link

@gjj gjj commented Jul 18, 2015

@jbongaarts can confirm this works! Have been trying to look for a solution for ages.

@bernardbr
Copy link

@bernardbr bernardbr commented Aug 12, 2015

@jiajian this works but not as well. In fact it suppresses the methods that conflict leaving only one in the documentation.

@caodaiming
Copy link

@caodaiming caodaiming commented Mar 11, 2016

action added route("api/getstudent")

@KarunaGovind
Copy link

@KarunaGovind KarunaGovind commented Apr 7, 2016

My workaround has been to use hashes to show the same endpoint multiple times. Does the job even though swaggerui may look a little untidy. So my json output ends up being something like:

/user/login#step1: { post: { } }, /user/login#step2: { post: { } },

Obviously the hashes don't get POSTed and the APIs work as expected.

@philals
Copy link

@philals philals commented May 20, 2016

@KarunaGovind Can you post the C# for this endpoint? Or do you edit the JSON manually? Thanks.

@KarunaGovind
Copy link

@KarunaGovind KarunaGovind commented May 20, 2016

@philals I didn't do this in C# but going by your above example, maybe something like this:

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

routes.MapHttpRoute(
    name: "another one with same endpoint",
    routeTemplate: "api/{controller}/{id}#blah2",
    defaults: new { id = RouteParameter.Optional }
);

routes.MapHttpRoute(
    name: "yet another one",
    routeTemplate: "api/{controller}/{id}#blah3",
    defaults: new { id = RouteParameter.Optional }
);
@philals
Copy link

@philals philals commented May 23, 2016

Thanks @KarunaGovind

@hanssonfredrik
Copy link

@hanssonfredrik hanssonfredrik commented Jun 29, 2016

Add {action} to your route and it will work as supposed.

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

@candoumbe
Copy link

@candoumbe candoumbe commented Jul 22, 2016

Hi,
What would be the equivalent workaround for aspnetcore 1.0 ?
Thanks in advance

@durgeshanju
Copy link

@durgeshanju durgeshanju commented Jul 27, 2016

Hey
It worked. But it considered only the first method, If there are multiple.
Thanks

@candoumbe
Copy link

@candoumbe candoumbe commented Jul 27, 2016

Could you be more specific please ?
Did you mean "it worked" [on asp.netcore] ?

@durgeshanju
Copy link

@durgeshanju durgeshanju commented Jul 27, 2016

@candoumbe No. I did not try on aspnetcore 1.0.
I am referring to answer posted by @domaindrivendev .

@Benraay
Copy link

@Benraay Benraay commented Sep 28, 2016

hanssonfredrik 's solution with the Action saved my life ! thanks

@xtrmstep
Copy link

@xtrmstep xtrmstep commented Oct 11, 2016

There is one more solution to this, but it's a deviation from Swagger 2.0 spec.

You can implement ISwaggerProvider using the current class SwaggerGenerator. To cut long story short, just copy-paste the class from the original source to ISwaggerProvider implementation and initialize private fields using reflection. The provider can be added by means of the standard way through the configuration. In your implementation you should change only one line of code to allow multiple operations (example):
.GroupBy(apiDesc => apiDesc.RelativePathSansQueryString())
to
.GroupBy(apiDesc => apiDesc.RelativePath)

That will solve the problem w/o necessity to merge actions.

@neuhoffm
Copy link

@neuhoffm neuhoffm commented Nov 26, 2016

The first part of this article solved this issue for me: https://docs.microsoft.com/en-us/azure/app-service-api/app-service-api-dotnet-swashbuckle-customize. No need to change my API, just add a custom operation filter that generates unique ids for each operation for swagger. API remains unchanged otherwise.

@fabriciorissetto
Copy link

@fabriciorissetto fabriciorissetto commented Nov 30, 2016

@neuhoffm thanks for sharing this, but the solution described there doesn't solve the problem of the "multiple operations with the same path". Instead it resolves the problem of the conflicting "operationIds" but the paths are not the same in that example.

@jhonwritter
Copy link

@jhonwritter jhonwritter commented Jun 12, 2017

How to add the custom headers based on controllers.
Lets say ..
Controller Name : Controller1 , Custom header : Header1
Controller Name : Controller2 Custom header : Header2
The headers should only display for all the apis under specific controllers

@Monte-Christo
Copy link

@Monte-Christo Monte-Christo commented Aug 25, 2018

In ASP.NET Corec apps, configure ResolveConflictingActions in your Setup class,. Add this to the ConfigureServices() method:

 services.AddSwaggerGen(c =>
{
other configs...;
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
});

I verified that it works for dotnet Core 2.1. And it requires no other changes. Ugly but simple.

@rikkiprince
Copy link

@rikkiprince rikkiprince commented Oct 26, 2018

Just because I had this issue and no-one seemed to have mentioned it here, you can also use LINQ fluent selectors other than .First(). For example, the first one in my list of apiDescriptions was actually the one I didn't want to display.

I ended up using:

c.ResolveConflictingActions(apiDescriptions => apiDescriptions
    .Where(x => x.RelativePath.Contains("parameterName")).First());

This selects the endpoint which contains a certain parameter, and in case there's multiple of those, takes the first of that sub-list.

@TheMagnificent11
Copy link

@TheMagnificent11 TheMagnificent11 commented Nov 5, 2019

@philals I didn't do this in C# but going by your above example, maybe something like this:

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

routes.MapHttpRoute(
    name: "another one with same endpoint",
    routeTemplate: "api/{controller}/{id}#blah2",
    defaults: new { id = RouteParameter.Optional }
);

routes.MapHttpRoute(
    name: "yet another one",
    routeTemplate: "api/{controller}/{id}#blah3",
    defaults: new { id = RouteParameter.Optional }
);

I did something similar and got it to work.

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}");

    routes.MapRoute(
        name: "single",
        template: "{controller=Home}/{id}");
});
@mishrsud
Copy link

@mishrsud mishrsud commented Nov 6, 2019

@philals I didn't do this in C# but going by your above example, maybe something like this:

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

routes.MapHttpRoute(
    name: "another one with same endpoint",
    routeTemplate: "api/{controller}/{id}#blah2",
    defaults: new { id = RouteParameter.Optional }
);

routes.MapHttpRoute(
    name: "yet another one",
    routeTemplate: "api/{controller}/{id}#blah3",
    defaults: new { id = RouteParameter.Optional }
);

Tried this in an asp.net core 2.2 application and although neither swashbuckle nor asp.complain, the action with the route with anchor (e.g. #blah) never gets hit. The only way to reach that action is to introduce a path segment

@davidbuckleyni
Copy link

@davidbuckleyni davidbuckleyni commented May 26, 2020

Is this really still the same in 20 20 you need to be able to pass different parameters to multiple get statements within the one controller.

@spaasis
Copy link

@spaasis spaasis commented May 26, 2020

EDIT: Clarification

I had two POST RPC methods in one controller (Core 3.1), and solved the issue by adding the action parameter to the Controller Route attribute:

namespace WebApi.ShippingDocuments {
    [Route("api/v{version:apiVersion}/[controller]/[action]")]
    [ApiController]
    public class ShippingDocumentsController : BaseController {
        [HttpPost]
        public async Task<int> DoThingX(){ }
        [HttpPost]
        public async Task<int> DoThingY() { }
    }
}

Results in
image

@candoumbe
Copy link

@candoumbe candoumbe commented May 26, 2020

I had two POST methods in one controller (Core 3.1), and solved the issue by adding the action parameter to the Controller Route attribute:

namespace WebApi.ShippingDocuments {
    [Route("api/v{version:apiVersion}/[controller]/[action]")]
    [ApiController]
    public class ShippingDocumentsController : BaseController {
        [HttpPost]
        public async Task<int> CreateNewShippingDocument(CreateShippingDocumentCommand cmd){ }
        [HttpPost]
        public async Task<int> UpdateShippingDocument(UpdateShippingDocumentCommand cmd) { }
    }
}

Results in
image

The issue you have is because the two actions have the same verb.
If you're updating a resource, try using HttpPut instead of HttpPost and you won't need to add the [action] placeholder.

@jezzipin
Copy link

@jezzipin jezzipin commented Sep 4, 2020

I can't believe that this is still an issue in 2020. Obviously one of the main reasons to use Swashbuckle is for Api document gen however, you shouldn't have to use bad programming practices (code duplication) in order to show that one method in a controller is shared between multiple Api versions. Case in point:

I have a login controller in v1.0 of my Api. Due to a change in another controller I have to roll v1.1 of my Api. Because the login controller has not changed I should be able to decorate the controller with two [ApiVersion] attributes to reflect this and leave the method as is because the logic has not changed. In .Net core, doing this will expose our '/login' path to version 1.0 and 1.1 independently without any other changes. The issues occur however when you get to version 2.0 of the Api. In this instance we need to make a change to the login method for v2.0 only whilst still supporting the old login methods used by other consumers. To do this, we decorate our controller with the new ApiVersion to reflect that it supports 3 versions of the Api. We then add MapToApiVersion("2.0") to the new method and equally on the pre-exisiting method for the previous versions we add two attributes MapToApiVersion("1.0") and MapToApiVersion("1.1"). Because the methods are allocated to specific versions they are allowed to have the same route however, at this point Swashbuckle fails to generate complaing that multiple operations have the same path. The only way to get around this is to create dummy public methods that call into the same private method behind the scenes in order to make Swashbuckle think the method names and path to not clash which is code duplication purely to satisfy a poor decision in the Swagger spec.

Example of the above can be found here:

https://github.com/jezzipin/core-api-with-swagger/tree/master/CoreApiWithSwagger

@Jin-K
Copy link

@Jin-K Jin-K commented Oct 18, 2020

Same for me @jezzipin, can't believe I lost more than a day looking for a solution, reading ugly workarounds everywhere, and not finding a concrete & complete example of how to resolve this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet