domaindrivendev / Swashbuckle.AspNetCore

Swagger tools for documenting API's built on ASP.NET Core
MIT License
5.2k stars 1.29k forks source link

"Conflicting method/path combination" error when deconflicting Actions via the `Consumes` attribute #2270

Open jacobjmarks opened 2 years ago

jacobjmarks commented 2 years ago

I am currently attempting to utilise the following Controller, based on Microsoft's own documentation to support a single HTTP endpoint that can digest both Form and Body requests.

[ApiController, Route("/")]
public class MyController : ControllerBase
{
    [HttpPost("form"), Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
    public IActionResult PostForm([FromForm] MyForm form) => Ok(form);

    [HttpPost("form"), Consumes("application/json")]
    public IActionResult PostBody([FromBody] MyForm form) => Ok(form);
}

Functionally, this works perfectly. Requests are routed to either Action method based on the incoming Content-Type.

The Swagger specification for this endpoint, I would expect to be defined simply as follows:

/form:
  post:
    requestBody:
      content:
        application/x-www-form-urlencoded:
          schema:
            $ref: '#/components/schemas/MyForm'
        multipart/form-data:
          schema:
            $ref: '#/components/schemas/MyForm'
        application/json:
          schema:
            $ref: '#/components/schemas/MyForm'

However, during schema generation, a Conflicting method/path combination error is encountered;

Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException:
Conflicting method/path combination "POST form" for actions - PostForm, PostBody.
Actions require a unique method/path combination for Swagger/OpenAPI 3.0. Use ConflictingActionsResolver as a workaround

This issue has been previously discussed within #1798 however I feel the proposed workarounds - either "taking the first definition" during specification generation (producing an inaccurate spec) or splitting the single endpoint into two (modifying functionality) - are not suitable.

  1. Will support be implemented which can suitably handle such Controller Actions, which are being deconflicted with the Consumes attribute?
  2. How might I otherwise (reliably) utilise the SwaggerGenOptions.ResolveConflictingActions() extension method to achieve my desired schema which accurately represents all supported content types?

~ .NET SDK: 6.0.100 Swashbuckle.AspNetCore: 6.2.3

tbetts42 commented 1 year ago

Hi @jacobjmarks, I realize it's been over a year since this was posted, but the issue is still open, and we ran into something similar.

We were able to handle both [FromBody] and [FromForm] for a single route. We used a combination of SwaggerGenOptions.ResolveConflictingActions() and a new attribute that implemented IActionConstraint.

First, as you already have, you need two separate functions in your controller class, with the same route and HTTP method, but different function names. One has a [FromBody] parameter and the other has [FromForm].

Second, put all your Consumes content types on the FromBody function. Remove the Consumes attribute from the FromForm function. Having multiple content types causes Swashbuckle to create requestBody.content for each content type.

        "requestBody": {
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/MyForm"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MyForm"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/MyForm"
              }
            }
          }
        },

Third, handle the conflicting actions detected by Swashbuckle. Because we put all of the Consumes attributes on one function, we can use that as our filter to find the correct action to use.

.AddSwaggerGen(options =>
{
    options.ResolveConflictingActions(apiDescriptions =>
                                      apiDescriptions.First(d => d.ActionDescriptor.ActionConstraints.Any(ac => ac is ConsumesAttribute)));
})

In our code, we have additional checks, such as only handling a certain .RelativePath because we only needed this for backwards compatibility of our API, and we don't want to create new endpoints following this pattern. You may need to experiment and find what works for you.

Lastly, we need to get asp.net MVC to use the FromForm function if we receive form data. For that we created an IActionConstraint attribute:

public class FormContentTypeAttribute : Attribute, IActionConstraint
{
    public int Order => 0;

    public bool Accept(ActionConstraintContext ctx) =>
        ctx.RouteContext.HttpContext.Request.HasFormContentType;
}

This says the function handles a request with form content, and Order = 0 means it takes precedent over any other function with the same route and HTTP method. This effectively gets .net to ignore [Consumes("multipart/form-data")] it sees on the FromBody function.

The two functions look something like this:

    [Route("form")]
    [HttpPost]
    [Consumes("application/x-www-form-urlencoded", "application/json", "multipart/form-data")]
    public IActionResult PostFromBody([FromBody] MyForm form) => Ok(form);

    [Route("form")]
    [HttpPost]
    [FormContentType]
    public IActionResult PostFromForm([FromForm] MyForm form) => Ok(form);

Credit for the IActionConstraint implementation from Stack Overflow: https://stackoverflow.com/questions/51673361/how-to-combine-frombody-and-fromform-bindingsource-in-asp-net-core

lus commented 1 year ago

Hello.

I currently face the same issue, however, I cannot apply the mentioned workaround as I use different request schemas for the different content types. Is there any ETA on when this will be fixed or does somebody know about a workaround that can solve this issue?

github-actions[bot] commented 3 months ago

This issue is stale because it has been open for 60 days with no activity. It will be automatically closed in 14 days if no further updates are made.