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 handlingdefault values
at swagger ui, in order to achieve this create new folder namedFilters
underSwagger
folder and then addAddDefaultValuesOperationFilter.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);
}
}