asp.net-mvcasp.net-coreswashbuckleapi-versioning

how to generate default versions swagger collection?


I'm trying to create a versioned swagger using the Asp Versioning Mvc ApiExplorer library.

I followed the basic guide documentation and applied the Operation Filter, but even though I specified the default api version, if there is an api with the 'ApiVersion' attribute, it will not generate for the api without the attribute. (If there is only an API with no attribute, it will be created with the default version).

It would be nice if APIs without attributes could always be generated with the default version.

Here's my Operation Filter.

 public void Apply( OpenApiOperation operation, OperationFilterContext context )
    {
        var apiDescription = context.ApiDescription;
        operation.Deprecated |= apiDescription.IsDeprecated();

        foreach ( var responseType in context.ApiDescription.SupportedResponseTypes )
        {
            var responseKey = responseType.IsDefaultResponse
                ? "default"
                : responseType.StatusCode.ToString();
            var response = operation.Responses[responseKey];

            foreach ( var contentType in response.Content.Keys )
            {
                if ( !responseType.ApiResponseFormats.Any( x => x.MediaType == contentType ) )
                {
                    response.Content.Remove( contentType );
                }
            }
        }

        if ( operation.Parameters == null )
        {
            return;
        }
        
        foreach ( var parameter in operation.Parameters )
        {
            var description = apiDescription.ParameterDescriptions
                .First( p => p.Name == parameter.Name );

            parameter.Description ??= description.ModelMetadata?.Description;

            if ( parameter.Schema.Default == null && description.DefaultValue != null )
            {
                var json = JsonSerializer.Serialize(
                    description.DefaultValue,
                    description.ModelMetadata.ModelType );
                parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson( json );
            }

            parameter.Required |= description.IsRequired;
        }
    }

Controller

[ApiController]
[ApiExplorerSettings(GroupName = "Version")]
[Route("version")]
public class VersionController : ControllerBase
{
    /// <summary>
    /// For Testing. (i want this api document, but not generated)
    /// </summary>
    /// <response code="200">Product retrieved</response>
    [HttpGet]
    public async Task<IActionResult> Version1Get()
    {
        return Ok("version1.0 response");
    }
    
    /// <summary>
    /// Fot Testing 1.2.
    /// </summary>
    /// <response code="200">Product retrieved</response>
    [HttpGet]
    [ApiVersion(1.2)]
    public async Task<IActionResult> Version2Get()
    {
        return Ok("version1.2 response");
    }
}

Versioning Setting

{
    options.ApiVersionReader = new HeaderApiVersionReader("x-ms-version");

    options.DefaultApiVersion = new ApiVersion(1, 0);
    
    options.ReportApiVersions = true;
    options.AssumeDefaultVersionWhenUnspecified = true;

}).AddMvc().AddApiExplorer(
    options =>
    {
        options.GroupNameFormat = "VV";

        options.FormatGroupName = (group, version) => $"{group} - {version}";
        options.DefaultApiVersion = new ApiVersion(1, 0);

        options.AssumeDefaultVersionWhenUnspecified = true;
    } );

Solution

  • This design is intentional. API Versioning requires that API versions be explicit. If you adopted API Versioning and were then required to make application changes to keep things working, that would be painful and could prevent adoption.

    Every API has some version, even if it's not formally declared. Once API Versioning is enabled, that default version is surfaced. You can call or label what you like, but it was always there. Now, it just has a formal value.

    Conversely, imagine that you reach V3 or V4 and you want to sunset the original V1 version. If the default API version was always included, then you wouldn't be able to remove it. The default API version only applies when no other attribute or convention defines any API versions. Once you start defining other API versions, you are required to include the original API versions to express the intent that you intend to preserve that version as opposed to removing it. This issue only occurs if you interleave multiple versions on the same API.

    If you want to implicitly include the default API version on all controllers, then the best approach is to use a custom convention.

    internal sealed class DefaultApiVersionConvention : IControllerConvention
    {
        private readonly ApiVersion defaultApiVersion;
    
        public DefaultApiVersionConvention() : this(new ApiVersion(1.0)) { }
    
        public DefaultApiVersionConvention(ApiVersion defaultApiVersion)
            => this.defaultApiVersion = defaultApiVersion;
    
        public bool Apply(
            IControllerConventionBuilder builder,
            ControllerModel controller) =>
            builder.HasApiVersion(this.defaultApiVersion);
    }
    

    You can then wire it up in the configuration with:

    builder.Services
           .AddApiVersioning(
            options =>
            {
                // an explicit value is ok, but...
                // 1.0 == ApiVersion.Default == default value
                options.DefaultApiVersion = new ApiVersion(1, 0);
                options.ApiVersionReader = new HeaderApiVersionReader("x-ms-version");
                options.ReportApiVersions = true;
                options.AssumeDefaultVersionWhenUnspecified = true;
            })
           .AddMvc(options => options.Conventions.Add(new DefaultApiVersionConvention()))
           .AddApiExplorer(
            options =>
            {
                options.GroupNameFormat = "VV";
                options.FormatGroupName = (group, version) => $"{group} - {version}";
            });
    

    This will make the rest of your OpenAPI (e.g. Swagger) configuration to work without any additional changes.


    Sidebar: You can specify 1.0 to be explicit, but that is the default value and equivalent to ApiVersion.Default. While you can also configure these values in the API Explorer, you shouldn't. They will automatically derive to be the same configured values in the ApiVersioningOptions.