Swashbuckle.aspnetcore: Actions require unique method/path combination for Swagger

Created on 4 May 2017  Â·  29Comments  Â·  Source: domaindrivendev/Swashbuckle.AspNetCore

my code:

[Route("api/[controller]/[action]")]
public class DemoController : Controller
{
        [HttpGet]
        public JsonResult GetFoo(string p1)
        {
               return Json(p1)
        }
        [HttpGet]
        public JsonResult GetFoo(int p2)
        {
               return Json(p2)
        }
}
public class Startup
{     
         ........
        public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            //Swagger - Enable this line and the related lines in Configure method to enable swagger UI
            services.AddSwaggerGen(options =>
            {
                options.SwaggerDoc("v1", new Info { Title = "ChiakiYu API", Version = "v1" });
                options.DocInclusionPredicate((docName, description) => true);
            });           
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "defaultWithArea",
                    template: "{area}/{controller=Home}/{action=Index}/{id?}");

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

            // Enable middleware to serve generated Swagger as a JSON endpoint
            app.UseSwagger();
            // Enable middleware to serve swagger-ui assets (HTML, JS, CSS etc.)
            app.UseSwaggerUI(options =>
            {
                options.SwaggerEndpoint("/swagger/v1/swagger.json", "ChiakiYu API V1");
            }); //URL: /swagger
        }
    }



md5-09b590a544b6281e06730287662e06bd



ERROR 2017-05-04 12:55:53,753 [4    ] Microsoft.AspNetCore.Server.Kestrel      - Connection id "0HL4IL7AH9QPA": An unhandled exception was thrown by the application.
System.NotSupportedException: HTTP method "GET" & path "api/Demo/GetFoo" overloaded by actions - ChiakiYu.Controllers.DemoController.GetFoo (ChiakiYu.Web.Core),ChiakiYu.Controllers.DemoController.GetFoo (ChiakiYu.Web.Core). Actions require unique method/path combination for Swagger
   at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.CreatePathItem(IEnumerable`1 apiDescriptions, ISchemaRegistry schemaRegistry)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwagger(String documentName, String host, String basePath, String[] schemes)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.<Invoke>d__6.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Builder.RouterMiddleware.<Invoke>d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware`1.<Invoke>d__18.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware`1.<Invoke>d__18.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware.<Invoke>d__7.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.<Invoke>d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Server.IISIntegration.IISMiddleware.<Invoke>d__8.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Hosting.Internal.RequestServicesContainerMiddleware.<Invoke>d__3.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Server.Kestrel.Internal.Http.Frame`1.<RequestProcessingAsync>d__2.MoveNext()

if I use the same method name,
how should I do ?

Most helpful comment

Well, OK, after some experimental programming, I found the place to configure ResolveConflictingActions in ASP.NET Core apps. 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. However, this is still only a work-around for me.

All 29 comments

I worked around this by using a FilterModel parameter on one method instead of overloading the method and using primitive parameters.

public IViewModel<User, long>[] Get([FromQuery] UserFilterModel filter)

That said, it would be nice for Swashbuckle to support overloaded endpoint methods, somehow.

(Also, if a maintainer is reading this, then know that supporting...

[AcceptVerbs("SEARCH")]
public IViewModel Search([FromBody] BigNastyQuery) {

... would be like Christmas. 🎄 😛)

is this issue being looked at?

Can I also understand if this issue is being looked at?

I fixed this by following the example found here: https://github.com/Microsoft/aspnet-api-versioning/blob/master/samples/aspnetcore/SwaggerSample/V2/Controllers/OrdersController.cs#L24-L47

I found that by supplying a Name to [HttpWhatever(Name = "whatever")] I did not encounter this issue.

This is not a bug. It should be clear from the exception that this is NotSupported by design.

Swashbuckle creates a 1:1 mapping between ASP.NET Core actions and Swagger Operation objects. If you read through the Swagger 2.0 specification, you'll see that multiple Operations CANNOT share the same path and HTTP method and therefore, multiple actions can't share the same path and HTTP method.

There is another example of the issue when we have actions with the same route path, different method names and IActionConstraint applied. I believe it would be better to avoid exception at least in this case. For now I'm forced to use [ApiExplorerSettings(IgnoreApi = true)] for one of the actions. It works fine but not elegant.

Don’t create a PR that removes the exception. IMO, it’s the appropriate behavior.

To avoid having to explicitly ignore every conflicting action, you can apply a global strategy with the _ResolveConflictingActions_ config setting

Okay, np. Just to be sure you understand my case. It is described here but I have a filter value in http session instead of http header and I use IActionConstraint instead of suggested ways in the article. Thanks for fast reaction!

Would someone please care to elaborate how to "apply a global strategy with the ResolveConflictingActions config setting". Where in my ASP.NET Core Web API Startup class do i put this?

Well, OK, after some experimental programming, I found the place to configure ResolveConflictingActions in ASP.NET Core apps. 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. However, this is still only a work-around for me.

I suggest you try and avoid needing to do that by properly writing your API signatures. I consider this a hack around the correct way.

I see. Well, this "hack" is suggested by Kevin Dockx from Pluralsight (Building a RESTful API with ASP.NET Core) for versioning APIs though custom media types. He uses action constraints on Accept and Content-Type headers to disambiguate the controller methods.

A lot of people do it because it's a quick easy fix. I have also found that
a lot of the pluralsight people will take shortcuts and just show you the
shortest way to do something. Have a constants class to store the names for
your routes and then properly name all your routes and you shouldn't need
that custom function.

On Sat, Aug 25, 2018, 10:23 AM Monte Christo notifications@github.com
wrote:

I see. Well, this "hack" is suggested by Kevin Dockx from Pluralsight
(Building a RESTful API with ASP.NET Core) for versioning APIs though
custom media types. He uses action constraints on Accept and Content-Type
headers to disambiguate the controller methods.

—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/390#issuecomment-415976588,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ACzG62a3pjuaHz6H4PZ4cu-tDqsO4YY-ks5uUWvWgaJpZM4NQOk0
.

@VictorioBerra What would you do in the case where you want to post data to the same url in different forms? For example:

[HttpPost("users")]
[Consumes("application/json")]
public async Task<ActionResult> UploadJson(Guid subscriptionId, RawData data)
{
}

[HttpPost("users")]
[Consumes("multipart/form-data")]
public async Task<ActionResult> UploadFile(Guid subscriptionId, IFormFile file)
{
}

Same url but different content type handled in a different way in each method.

This works perfectly well via postman but the swagger json cannot be generated.

@simonvane

Its not the servers job to try and figure out where to send the request, its the servers job to have an easy to navigate and explicit schema. I would change those to: [HttpPost("users", Name = UserControllerRoute.PostJson)] and [HttpPost("usersfile", Name = UserControllerRoute.PostFile)]. It never hurts to have to be more explicit in your calling code wether it be Angular, jQuery, etc.

Those endpoints seem very different. A JSON payload does not seem to be a substitute for a file payload. For example, like posting a CSV body could use a different MVC Input Formatter and then you could wire that up in your Startup.

Thanks @VictorioBerra . The server is able to figure it out fine based on the content type header, it's just swashbuckle that can't figure it out.

The data is identical in the two cases just transported by json or csv. Two different client types need to provide teh data like that for historical reasons. Does that change your opinion?

I'm not sure what I would gain from an input formatter for the file. Using IFormFile works well but I might be missing something. Would you mind explaining a bit more please?

@simonvane

https://github.com/WebApiContrib/WebAPIContrib.Core/tree/master/src/WebApiContrib.Core.Formatter.Csv

This works with Swagger. Use that, wire it up in your startup as show in the README. Additionally, you could use the [Produces("text/csv")] or [Produces("text/json")] over all your controllers where you need it or you could let the formatter decide when it can be used.

That project wires up a CSV input and output formatter for you. The output formatter is on any controller action that returns an IEnumerable IIRC. That might be the same for the input formatter?

I would still be more explicit on my controllers. many times, the ViewModel is different enough to necessitate having a different endpoint for a different input/output format. For example, CSV is very flat, and you might have to get really custom with serializing and deserializing it (multiple columns with the same name wont fly for example).

Late to the party, but wanted to clarify for myself.

So if I have two API versions, v1.0 and v2.0, and have ValuesController with GetV1 and GetV2. Ideally, both should be resolved at the same Values endpoint with the "GET" verb, but the version of response you receive is dictated by the header value (in my case I want to use "api-verson=2.0" - how do I handle this scenario?

This seems like a genuine case where you want the methods/verbs signature to match but want to get the different response (or do I misunderstand versioning?)

Thanks for any pointers

@rmccabe24 Im not sure what you mean? The version dictates which action method to actually invoke. https://github.com/microsoft/aspnet-api-versioning/wiki/Versioning-via-the-URL-Path#aspnet-web-api

if i use thise example:
`
[HttpGet]
[Route("version")]
[MapToApiVersion("1.0")]
public string GetV1() => "version 1.0";

    [HttpGet]
    [Route("version")]
    [MapToApiVersion("2.0")]
    public string GetV2() => "version 2.0";

    [HttpGet]
    [Route("version")]
    [MapToApiVersion("2.1")]
    public string GetV21() => "version 2.1";

`
i get the stated error message. which i think as wrong. i think this is what @rmccabe24 stated.

Also late to the party, this issue does not seem to be resolved.
I have seen 1 solution in the issue which is a workaround.
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());

What is the proper solution? for me this started acting up when I updated the library to the newest version (5.0.0.rc3).

Same Issue here , I have two routes

api/Customer?prop1=test
api/Customer?prop12=test

Get this exception - why? this is totally valid ?

Same Issue here , I have two routes

api/Customer?prop1=test
api/Customer?prop12=test

Get this exception - why? this is totally valid ?

Do you have 1 or 2 functions?
In this case I would have 1 function with 2 properties prop1 and prop12

@jeanpoelie I have two endpoints (methods) marked with [HTTPGet] which take in different query params of the same type .

They look up a customer based on 1 value or the other. It would be redundant to me to take in 2 parameters and have on or the other being null and then null checking.

@cloudb0x You cannot have 2 methods with the same parameter type and endpoint in Swagger by defualt, you either need an overload with 2 paramters or create the functions called api/CustomerProp1?prop1=value and api/CustomerProp12?prop12=value

I have /a/b/apple/foo and a/b/banana/foo mapped to two different controllers to actions with two different names that both take in the same parameters and I am seeing this. 5.0.0.rc4

[ApiController]
[Route("/a/b/apple"]
public class AppleController : ControllerBase
{
    ...
    [HttpGet("foo")]
    public async Task<IActionResult> AppleFoo(int id1, int id2) {}
}

...

[ApiController]
[Route("/a/b/banana"]
public class BananaController : ControllerBase
{
    ...
    [HttpGet("foo")]
    public async Task<IActionResult> BananaFoo(int id1, int id2) {}
}

From my understanding of the specs, this should be all well and good. Am I missing something?

@hammypants What if you add a name to both HttpGet attributes?

@VictorioBerra that works.

This works with Swagger. Use that, wire it up in your startup as show in the README. Additionally, you could use the [Produces("text/csv")] or [Produces("text/json")] over all your controllers where you need it or you could let the formatter decide when it can be used.

@VictorioBerra Produce is different from Consume.
it's part of the content-type header and it should be supported since asp.net core routing is supporting it. asking to use Produce which is about accept header is like shooting in the foot.

Was this page helpful?
0 / 5 - 0 ratings