From f1e3fc911fc4479a4558a1cdc16cd5736700622a Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 6 May 2024 06:41:51 -0700 Subject: [PATCH] Fix Swashbuckle configuration --- HotelListing.API/ConfigureSwaggerOptions.cs | 103 ++++++++++++++++++++ HotelListing.API/Program.cs | 61 +++++++----- HotelListing.API/SwaggerDefaultValues.cs | 65 ++++++++++++ 3 files changed, 204 insertions(+), 25 deletions(-) create mode 100644 HotelListing.API/ConfigureSwaggerOptions.cs create mode 100644 HotelListing.API/SwaggerDefaultValues.cs diff --git a/HotelListing.API/ConfigureSwaggerOptions.cs b/HotelListing.API/ConfigureSwaggerOptions.cs new file mode 100644 index 0000000..5f178a9 --- /dev/null +++ b/HotelListing.API/ConfigureSwaggerOptions.cs @@ -0,0 +1,103 @@ +namespace HotelListing.API; + +using Asp.Versioning.ApiExplorer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Text; + +/// +/// Configures the Swagger generation options. +/// +/// This allows API versioning to define a Swagger document per API version after the +/// service has been resolved from the service container. +public class ConfigureSwaggerOptions : IConfigureOptions +{ + private readonly IApiVersionDescriptionProvider provider; + + /// + /// Initializes a new instance of the class. + /// + /// The provider used to generate Swagger documents. + public ConfigureSwaggerOptions( IApiVersionDescriptionProvider provider ) => this.provider = provider; + + /// + public void Configure( SwaggerGenOptions options ) + { + // add a swagger document for each discovered API version + // note: you might choose to skip or document deprecated API versions differently + foreach ( var description in provider.ApiVersionDescriptions ) + { + options.SwaggerDoc( description.GroupName, CreateInfoForApiVersion( description ) ); + } + } + + private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription description ) + { + var text = new StringBuilder( "An example application with OpenAPI, Swashbuckle, and API versioning." ); + var info = new OpenApiInfo() + { + Title = "Example API", + Version = description.ApiVersion.ToString(), + Contact = new OpenApiContact() { Name = "Bill Mei", Email = "bill.mei@somewhere.com" }, + License = new OpenApiLicense() { Name = "MIT", Url = new Uri( "https://opensource.org/licenses/MIT" ) } + }; + + if ( description.IsDeprecated ) + { + text.Append( " This API version has been deprecated." ); + } + + if ( description.SunsetPolicy is { } policy ) + { + if ( policy.Date is { } when ) + { + text.Append( " The API will be sunset on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } + + if ( policy.HasLinks ) + { + text.AppendLine(); + + var rendered = false; + + for ( var i = 0; i < policy.Links.Count; i++ ) + { + var link = policy.Links[i]; + + if ( link.Type == "text/html" ) + { + if ( !rendered ) + { + text.Append( "

Links

" ); + } + } + } + + text.Append( "

Additional Information

" ); + info.Description = text.ToString(); + + return info; + } +} \ No newline at end of file diff --git a/HotelListing.API/Program.cs b/HotelListing.API/Program.cs index a7a987e..38bb6d4 100644 --- a/HotelListing.API/Program.cs +++ b/HotelListing.API/Program.cs @@ -1,8 +1,7 @@ using Asp.Versioning; -using Asp.Versioning.Conventions; +using HotelListing.API; using HotelListing.API.Configurations; using HotelListing.API.Contracts; -using HotelListing.API.Controllers; using HotelListing.API.Data; using HotelListing.API.Middleware; using HotelListing.API.Repository; @@ -32,32 +31,31 @@ builder.Services.AddIdentityCore() builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddTransient, ConfigureSwaggerOptions>(); +builder.Services.AddSwaggerGen(options => options.OperationFilter()); builder.Services.AddCors(options => { options.AddPolicy("AllowAll", b => b.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod()); }); -builder.Services.AddApiVersioning(options => -{ - options.AssumeDefaultVersionWhenUnspecified = true; - options.DefaultApiVersion = new ApiVersion(1, 0); - options.ReportApiVersions = true; - options.ApiVersionReader = ApiVersionReader.Combine( - new QueryStringApiVersionReader("api-version"), - new HeaderApiVersionReader("X-Version"), - new MediaTypeApiVersionReader("ver") - ); -}).AddMvc() -.AddApiExplorer( - options => - { - - options.GroupNameFormat = "'v'VVV"; - options.SubstituteApiVersionInUrl = true; - } -); +builder.Services + .AddApiVersioning(options => + { + options.AssumeDefaultVersionWhenUnspecified = true; + options.DefaultApiVersion = new ApiVersion(1, 0); + options.ReportApiVersions = true; + options.ApiVersionReader = ApiVersionReader.Combine( + new QueryStringApiVersionReader("api-version"), + new HeaderApiVersionReader("X-Version"), + new MediaTypeApiVersionReader("ver")); + }) + .AddMvc() + .AddApiExplorer(options => + { + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + }); builder.Host.UseSerilog((ctx, lr) => lr.WriteTo.Console().ReadFrom.Configuration(ctx.Configuration)); @@ -69,10 +67,12 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddAuthentication(options => { +builder.Services.AddAuthentication(options => +{ options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; // "Bearer" options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; -}).AddJwtBearer(options => { +}).AddJwtBearer(options => +{ options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, @@ -99,7 +99,18 @@ var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); - app.UseSwaggerUI(); + app.UseSwaggerUI(options => + { + var descriptions = app.DescribeApiVersions(); + + // build a swagger endpoint for each discovered API version + foreach (var description in descriptions) + { + var url = $"/swagger/{description.GroupName}/swagger.json"; + var name = description.GroupName.ToUpperInvariant(); + options.SwaggerEndpoint(url, name); + } + }); } app.UseMiddleware(); diff --git a/HotelListing.API/SwaggerDefaultValues.cs b/HotelListing.API/SwaggerDefaultValues.cs new file mode 100644 index 0000000..355ba28 --- /dev/null +++ b/HotelListing.API/SwaggerDefaultValues.cs @@ -0,0 +1,65 @@ +namespace HotelListing.API; + +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Text.Json; + +/// +/// Represents the OpenAPI/Swashbuckle operation filter used to document information provided, but not used. +/// +/// This is only required due to bugs in the . +/// Once they are fixed and published, this class can be removed. +public class SwaggerDefaultValues : IOperationFilter +{ + /// + 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 ); + + parameter.Description ??= description.ModelMetadata?.Description; + + if ( parameter.Schema.Default == null && + description.DefaultValue != null && + description.DefaultValue is not DBNull && + description.ModelMetadata is ModelMetadata modelMetadata ) + { + // REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330 + var json = JsonSerializer.Serialize( description.DefaultValue, modelMetadata.ModelType ); + parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson( json ); + } + + parameter.Required |= description.IsRequired; + } + } +} \ No newline at end of file -- 2.33.0.windows.2