Skip to content

Api Versioning

PSS®X integrates the ASPNET-API-Versioning feature and adapts to C# and JavaScript Static Client Proxies.

Use AddApiVersioning extensions and AbpAspNetCoreMvcOptions options class to register the api versioning in the ConfigureServices method of your module.

public override void ConfigureServices(ServiceConfigurationContext context)
{
    // https://github.com/dotnet/aspnet-api-versioning/issues/1029
    context.Services.AddTransient<IApiControllerFilter, NoControllerFilter>();
    context.Services.AddApiVersioning(options =>
    {
      options.AssumeDefaultVersionWhenUnspecified = true;
      options.ReportApiVersions = true;
    })
    .AddMvc()
    .AddApiExplorer(options =>
    {
      // The specified format code will format the version as "'v'major[.minor][-status]
      options.GroupNameFormat = "'v'VVV";
      // Note : this option is only necessary when versioning by url segment. the SubstitutionFormat
      // Can also be used to control the format of the API version in route templates
      options.SubstituteApiVersionInUrl = true;
    });

    Configure<AbpAspNetCoreMvcOptions>(options =>
    {
      options.ChangeControllerModelApiExplorerGroupName = false;
    });
}

Example usage at Controller.

namespace GridLab.PSSX.Odms.Models.v130
{
    [ApiVersion("13.0", Deprecated = false)]
    [RemoteService(Name = OdmsRemoteServiceConsts.RemoteServiceName)]
    [ControllerName("Models")]
    [Route("api/odms/models")]
    public class ModelController : OdmsController, IModelAppService
    {
        protected IModelAppService ModelAppService { get; }

        public ModelController(IModelAppService modelAppService)
        {
            ModelAppService = modelAppService;
        }

        [HttpPost]
        public virtual async Task<ModelWithDetailsDto> CreateAsync(CreateModelInput input)
        {
            return await ModelAppService.CreateAsync(input);
        }
        ....
    }
}
namespace GridLab.PSSX.Odms.Models.v131
{
    [ApiVersion("13.1", Deprecated = false)]
    [RemoteService(Name = OdmsRemoteServiceConsts.RemoteServiceName)]
    [ControllerName("Models")]
    [Route("api/odms/models")]
    public class ModelController : OdmsController, IModelAppService
    {
        protected IModelAppService ModelAppService { get; }

        public ModelController(IModelAppService modelAppService)
        {
            ModelAppService = modelAppService;
        }

        [HttpPost]
        public virtual async Task<ModelWithDetailsDto> CreateAsync(CreateModelInput input)
        {
            return await ModelAppService.CreateAsync(input);
        }
    }
}

Configure Swagger

Create new folder named Swagger and then add new class ConfigureSwaggerOptions.cs at GridLab.PSSX.<ModuleName> project.

using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;

namespace GridLab.PSSX.ModuleName.Swagger
{
    public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
    {
        readonly IApiVersionDescriptionProvider provider;

        public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => this.provider = provider;

        public void Configure(SwaggerGenOptions options)
        {
            foreach (var description in provider.ApiVersionDescriptions)
            {
                options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
            }
        }

        static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
        {
            var info = new OpenApiInfo()
            {
                Title = "ModuleName API",
                Version = description.ApiVersion.ToString(),
                Contact = new OpenApiContact()
                {
                    Name = "PSS®X",
                    Email = "info@gridlab.io",
                    Url = new Uri("https://gridlab.io"),
                },
                License = new OpenApiLicense()
                {
                    Name = "GPL",
                    Url = new Uri(uriString: "http://www.gnu.org/licenses/gpl-3.0.html")
                },
                TermsOfService = new Uri(uriString: "https://gridlab.io/general/terms-of-use.html")
            };

            if (description.IsDeprecated)
            {
                info.Description += " This API version has been deprecated.";
            }

            return info;
        }
    }
}
  • Replace existing context.Services.AddAbpSwaggerGen with following in order to configure swagger ui and open-api-spec generation
context.Services.AddAbpSwaggerGen(options =>
{
    // Add a custom operation filter which sets default values
    options.OperationFilter<AddDefaultValuesOperationFilter>();

    options.CustomSchemaIds(type => type.FullName);

    options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
    {
        Type = SecuritySchemeType.OAuth2,
        Flows = new OpenApiOAuthFlows
        {
            AuthorizationCode = new OpenApiOAuthFlow
            {
                AuthorizationUrl = new Uri($"{configuration["Swagger:Authority"].TrimEnd('/')}{"/connect/authorize".EnsureStartsWith('/')}"),
                Scopes = new Dictionary<string, string>
                {
                    {"ModulName", "ModulName API"}
                },
                TokenUrl = new Uri($"{configuration["Swagger:Authority"].EnsureEndsWith('/')}connect/token")
            }
        }
    });

    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "oauth2"
                }
            },
            Array.Empty<string>()
        }
    });

    options.HideAbpEndpoints(); // Hides ABP related endpoints at Swagger UI
});

Update oauth client configuration as well as versioning stragety at swagger ui middleware

public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
    // ...
    app.UseAbpSwaggerUI(options =>
    {
        var provider = app.ApplicationServices.GetRequiredService<IApiVersionDescriptionProvider>();
        // Build a swagger endpoint for each discovered API version
        foreach (var description in provider.ApiVersionDescriptions)
        {
            options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
        }

        options.OAuthClientId(context.GetConfiguration()["Swagger:ClientId"]);
        // options.OAuthClientSecret(context.GetConfiguration()["Swagger:ClientSecret"]);
    });
    // ...
}

Update appsettings.json accordingly

"Swagger": {
  "Authority": "https://localhost:44324", // Note port number needs to adjusted according to project requirement
  "ClientId": "ModuleName_Swagger",
  "ClientSecret": "1q2w3e*" // Swagger is non-public client therefore no need to specify secret, however it can be adjusted according to project needs
}
  • Add DefaultValuesOperation filter for handling default values at swagger ui, in order to achieve this create new folder named Filters under Swagger folder and then add AddDefaultValuesOperationFilter.cs.
using Asp.Versioning.ApiExplorer;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Linq;
using System.Text.Json;

namespace GridLab.PSSX.ModuleName.Swagger.Filters
{
    public class AddDefaultValuesOperationFilter : IOperationFilter
    {
        /// <summary>
        /// Applies the filter to the specified operation using the given context.
        /// </summary>
        /// <param name="operation">The operation to apply the filter to.</param>
        /// <param name="context">The current operation filter context.</param>
        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
            var apiDescription = context.ApiDescription;

            operation.Deprecated |= apiDescription.IsDeprecated();

            // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077
            foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
            {
                // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b7cf75e7905050305b115dd96640ddd6e74c7ac9/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L383-L387
                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;
            }

            // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412
            // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413
            foreach (var parameter in operation.Parameters)
            {
                var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);

                if (parameter.Description == null)
                {
                    parameter.Description = description.ModelMetadata?.Description;
                }

                if (parameter.Schema.Default == null && description.DefaultValue != null)
                {
                    // REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330
                    var json = JsonSerializer.Serialize(description.DefaultValue, description.ModelMetadata.ModelType);
                    parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
                }

                parameter.Required |= description.IsRequired;
            }
        }
    }
}
  • Add ApiDescriptionExtensions for checking is API deprecated or not.
public static partial class ApiDescriptionExtensions
{
    /// <summary>
    /// Gets the API version associated with the API description, if any.
    /// </summary>
    /// <param name="apiDescription">The <see cref="ApiDescription">API description</see> to get the API version for.</param>
    /// <returns>The associated <see cref="ApiVersion">API version</see> or <c>null</c>.</returns>
    public static ApiVersion? AspGetApiVersion(this ApiDescription apiDescription) => apiDescription.GetProperty<ApiVersion>();

    /// <summary>
    /// Gets a value indicating whether the associated API description is deprecated.
    /// </summary>
    /// <param name="apiDescription">The <see cref="ApiDescription">API description</see> to evaluate.</param>
    /// <returns><c>True</c> if the <see cref="ApiDescription">API description</see> is deprecated; otherwise, <c>false</c>.</returns>
    public static bool AspIsDeprecated(this ApiDescription apiDescription)
    {
        ArgumentNullException.ThrowIfNull(apiDescription);

        var metatadata = apiDescription.ActionDescriptor.GetApiVersionMetadata();

        if (metatadata.IsApiVersionNeutral)
        {
            return false;
        }

        var apiVersion = apiDescription.AspGetApiVersion();
        var model = metatadata.Map(Explicit | Implicit);

        return model.DeprecatedApiVersions.Contains(apiVersion);
    }
}