MestreDosMagros
MestreDosMagros

Reputation: 1028

.Net Core 3.1 swagger API versioning conflicting namespaces url

I'm using swagger to generate my API documentation, and now i needed to version some of my endpoints.

So i configured swagger to identify my versions and map the endpoint correctly. But swagger is lossing its track because i used the same class names on diferent namespaces and i get this error:

Conflicting method/path combination "GET api/v1/A" for actions - TesteSwagger.Controllers.B.AController.x (TesteSwagger),TesteSwagger.Controllers.A.AController.x (TesteSwagger). Actions require a unique method/path combination for Swagger/OpenAPI 3.0. Use ConflictingActionsResolver as a workaround

Here is the example i made to reproduce it

All my swagger packages are in version 6.0.2.

I`m using .Net Core 3.1 WebApi default empty template

Startup.cs:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            services.AddApiVersioning(
                options => 
                {
                    options.ReportApiVersions = true;
                    options.DefaultApiVersion = new ApiVersion(1, 0);
                    options.AssumeDefaultVersionWhenUnspecified = true;
                });

            services.AddVersionedApiExplorer(
                options =>
                {
                    options.GroupNameFormat = "'v'VVV";
                    options.SubstituteApiVersionInUrl = true;
                });

            services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();

            services.AddSwaggerGen(options =>
            {
                options.CustomSchemaIds(x => x.FullName);
                options.DescribeAllParametersInCamelCase();
                options.OperationFilter<SwaggerDefaultValues>();
                options.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseSwagger()
              .UseSwaggerUI(c =>
              {
                  c.DisplayRequestDuration();
                  foreach (var description in provider.ApiVersionDescriptions)
                  {
                      c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json",
                          description.GroupName.ToUpperInvariant());
                  }
              });

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

Swagger default configuration classes:

    public class SwaggerDefaultValues : IOperationFilter
    {
        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
            
        }
    }

    public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
    {
        public void Configure(SwaggerGenOptions options)
        {
        }
    }

A Controller (v1):

namespace TesteSwagger.Controllers.A
{
    [ApiController, ApiVersion("1.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class AController : ControllerBase
    {
        [HttpPost]
        [ProducesResponseType(typeof(B), (int)HttpStatusCode.OK)]
        public IActionResult x(A a) => Ok(new B());
    }

    public class A
    {
        public int Foo { get; set; }
    }

    public class B
    {
        public int Bar { get; set; }
    }
}

B Controller (v2):

namespace TesteSwagger.Controllers.B
{
    [ApiController, ApiVersion("2.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class AController : ControllerBase
    {
        [HttpPost]
        [ProducesResponseType(typeof(B), (int)HttpStatusCode.OK)]
        public IActionResult x(A a) => Ok(new B());
    }

    public class A
    {
        public int Foo { get; set; }
    }

    public class B
    {
        public int Bar { get; set; }
    }
}

The v1 URL of swagger loads just fine, only when i change it to v2 that this erros shows in the screen:

Fetch error
undefined /swagger/v2/swagger.json

I used the curl generated on the v1 URL to test and everything works just fine, just swagger dont get it

// works great
curl -X POST "https://localhost:44312/api/v1/A" -H  "accept: text/plain" -H  "Content-Type: application/json" -d "{\"foo\":0}"

// works great
curl -X POST "https://localhost:44312/api/v2/A" -H  "accept: text/plain" -H  "Content-Type: application/json" -d "{\"foo\":0}"

I really dont know if i doing something wrong or swagger really does not support this tipe of versioning.

Any ideas?

Upvotes: 4

Views: 2780

Answers (1)

Chris Martinez
Chris Martinez

Reputation: 4418

First, the names of types are largely irrelevant. Namespaces are not a concept that can typically be conveyed over HTTP and are highly dependent on the media type. Modern JavaScript does have modules, but it's not quite a namespace. Regardless, a .NET namespace will not be turned into a set of JavaScript modules to be represented in JSON; certainly, not by default.

The systemic issue here seems to be that you've defined a Swashbuckle configuration without any actual configuration. Three things have to line up for all the pieces to come together:

  1. Add the Versioned API Explorer - provides API versioning extensions that collates into groups a la the ApiDescription.GroupName using the formatted API version. The configuration in AddVersionedApiExplorer helps align that to the typical Swashbuckle formats and examples when versioning by URL segment.
  2. Add Swagger Endpoints - via UseSwaggerUI. It is expected to be one endpoint per API version. The segment value typically maps to the API version group name (e.g. formatted version)
  3. Add Swagger Documents - configures Swashbuckle to define an Open API (formerly Swagger) document per API version. The key of the document maps to the ApiDescription.GroupName and the segment of the endpoint defined in #2.

You have not provided any implementation or configuration for that in #3. As a result, Swashbuckle assumes there is only a single document. Since you have models with the same name and in the same document, you get a document generation error. You cannot have duplicate route paths or model names in the same document. You can, however, have duplicate model names across multiple versions when you define a document per API version.

Side Note: It is possible to have duplicate .NET type names within the same API version, but you will have to provide an alias. Namespace is never considered for distinctness. Unless it really doesn't make sense, I would recommend you use unique types/classes for model names within each set of versioned APIs.

The ASP.NET API Versioning Swagger Sample demonstrates what the configuration should look like in totality. A typical configuration will look something like:

public void Configure( SwaggerGenOptions options )
{
    // configure Swashbuckle to define a Swagger document per defined API version
    foreach ( var description in provider.ApiVersionDescriptions )
    {
        options.SwaggerDoc(
            description.GroupName,
            new OpenApiInfo()
            {
                Title = "API " + description.GroupName,
                Version = description.ApiVersion.ToString(),
            } );
    }
}

Upvotes: 1

Related Questions