Search code examples
swaggerswagger-uiswashbuckleasp.net-core-3.1swashbuckle.aspnetcore

Swashbuckle makes all route parameter mandatory for endpoints with same verb but multiple routes in asp.net core 3.1 api swagger ui


I'm working on asp.net core 2.2 project and upgrading to asp.net core 3.1 and also upgrading the Swashbuckle.AspNetCore to 5.0.0. After upgrading I can see change in the swagger generated endpoints.

I have a endpoint for [HttpDelete] with two different routes as shown below:

[HttpDelete("{id}")]
[HttpDelete("{id}/some/{anotherId}")]
public IActionResult Delete(int id, int anotherId) 
{
    return NoContent();
}

[HttpDelete("{id}")]

Only id parameter should be required here. But both id and anotherId parameter are also marked required here. This is wrong.

enter image description here

[HttpDelete("{id}/some/{anotherId}")]

Both id and anotherId parameter should be required here. This is correct.

enter image description here

Here is my Startup.cs:

ConfigureServices:

services.AddVersionedApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VV";
});

services.AddApiVersioning(options =>
{
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
    options.ApiVersionReader = new HeaderApiVersionReader("x-api-version");
});

var apiVersionDescriptionProvider =
    services.BuildServiceProvider().GetService<IApiVersionDescriptionProvider>();

services
    .AddSwaggerGen(options =>
{
    foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
    {
        options.SwaggerDoc(
            $"TestDocumentOpenAPISpecification{description.GroupName}",
            new Microsoft.OpenApi.Models.OpenApiInfo
            {
                Title = "Test Document API",
                Version = description.ApiVersion.ToString(),
                Description = "Test",
                Contact = new Microsoft.OpenApi.Models.OpenApiContact
                {
                    Email = "Test@test.com",
                    Name = "Test Team",
                    Url = new Uri("https://www.test.com")
                }
            });
    }

    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "Input your JWT Authorization header to access this API. Example: \"Authorization: Bearer {token}\"",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer"
    });
    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            new string[] { }
        }
    });

    options.DocInclusionPredicate((documentName, apiDescription) =>
    {
        var actionApiVersionModel = apiDescription.ActionDescriptor
        .GetApiVersionModel(ApiVersionMapping.Explicit | ApiVersionMapping.Implicit);

        if (actionApiVersionModel == null)
        {
            return true;
        }

        if (actionApiVersionModel.DeclaredApiVersions.Any())
        {
            return actionApiVersionModel.DeclaredApiVersions.Any(v =>
            $"TestDocumentOpenAPISpecificationv{v.ToString()}" == documentName);
        }

        return actionApiVersionModel.ImplementedApiVersions.Any(v =>
            $"TestDocumentOpenAPISpecificationv{v.ToString()}" == documentName);
    });

    //var xmlCommentsFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    //var xmlCommentsFullPath = Path.Combine(AppContext.BaseDirectory, xmlCommentsFile);

    //options.IncludeXmlComments(xmlCommentsFullPath);
});

Configure:

app.UseSwagger();

app.UseSwaggerUI(options =>
{
    foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
    {
        options.SwaggerEndpoint(
            $"/swagger/TestDocumentOpenAPISpecification{description.GroupName}/swagger.json",
            $"Test Document API - {description.GroupName.ToUpperInvariant()}");
    }
    options.RoutePrefix = string.Empty;

    options.DefaultModelExpandDepth(2);
    options.DefaultModelRendering(Swashbuckle.AspNetCore.SwaggerUI.ModelRendering.Model);
    options.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);
    options.DisplayRequestDuration();
    options.EnableValidator();
    options.EnableFilter();
    options.EnableDeepLinking();
    options.DisplayOperationId();
});

The swagger generated makes anotherId mandatory in both the routes. It's was not like that previously. I tried adding Name to both routes but still it fails. Please assist on where I'm wrong.


Solution

  • After some analysis I have made this to work using IOperationFilter.

    public class DeleteOperationFilter : IOperationFilter
    {
        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
            if (context.ApiDescription.HttpMethod == "DELETE" && context.MethodInfo.Name == "Delete")
            {
                foreach (var parameter in context.ApiDescription.ParameterDescriptions)
                {
                    if (parameter.RouteInfo == null)
                    {
                        operation.Parameters.Single(x => x.Name.Equals(parameter.Name)).Required = false;
                    }
                }
                return;
            }
        }
    }
    

    And adding it in the ConfigureServices,

    services.AddSwaggerGen(options =>
    {
        ...
        options.OperationFilter<DeleteOperationFilter>();
    };
    

    Am not sure this is the best way, but this works. Please correct me if I'm wrong.