Skip to content

Api Versioning

GMS² 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.Gmss.Cim.Models.v1
{
    [ApiVersion("1", Deprecated = false)]
    [RemoteService(Name = CimRemoteServiceConsts.RemoteServiceName)]
    [ControllerName("Models")]
    [Route("api/cim/models")]
    public class ModelController : CimController, 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.Gmss.Cim.Models.v2
{
    [ApiVersion("2", Deprecated = false)]
    [RemoteService(Name = CimRemoteServiceConsts.RemoteServiceName)]
    [ControllerName("Models")]
    [Route("api/cim/models")]
    public class ModelController : CimController, 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.Gmss.<ModuleName> project.

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

namespace GridLab.Gmss.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 = "GMS²",
                    Email = "noreply@gridlab.io",
                    Url = new Uri("https://gridlab.io"),
                },
                License = new OpenApiLicense()
                {
                    Name = "LGPL",
                    Url = new Uri(uriString: "https://www.gnu.org/licenses/lgpl-3.0.html")
                },
                TermsOfService = new Uri(uriString: "https://gridlab.io/general/terms-of-use")
            };

            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.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();

context.Services.AddAbpSwaggerGen(options =>
{
    // Add a custom operation filter which sets default values
    options.OperationFilter<AddDefaultValuesOperationFilter>();
    options.SchemaFilter<AddEnumSchemaFilter>();

    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>
                {
                    {"Gmss", "Gmss API"}
                },
                TokenUrl = new Uri($"{configuration["Swagger:Authority"]!.EnsureEndsWith('/')}connect/token")
            }
        }
    });

    options.AddSecurityRequirement(_ => new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecuritySchemeReference("oauth2"),
            new List<string>()
        }
    });

    // Include summary on UI
    foreach (var item in Directory.GetFiles(AppContext.BaseDirectory, "*.xml"))
    {
        options.IncludeXmlComments(item);
    }

    // options.HideAbpEndpoints();  // Hides ABP Related endpoints on 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 Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;

namespace GridLab.Gmss.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.AspIsDeprecated();

            // 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 is OpenApiSchema schema && 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);
                    schema.Default = JsonNode.Parse(json);
                }

                if (parameter is OpenApiParameter concreteParam && description.IsRequired)
                {
                    concreteParam.Required = true;
                }
            }
        }
    }
}
  • 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);
    }
}