From eb78e671bce544f378778ea994231fb13d45958a Mon Sep 17 00:00:00 2001 From: Xavier Date: Wed, 4 Mar 2026 19:30:36 -0800 Subject: [PATCH] Migrate OpenAPI docs from Swashbuckle to Scalar Replaced Swashbuckle.AspNetCore with Scalar.AspNetCore and Microsoft.AspNetCore.OpenApi across all sample projects. Updated service configuration to use AddOpenApi and Scalar endpoints. Removed custom Swashbuckle filters and configuration classes. Updated launch URLs to use Scalar's UI. Cleaned up copilot instructions and added notes for future OpenAPI improvements. --- .github/copilot-instructions.md | 2 +- Directory.Packages.props | 2 +- sample/MinApi/Program.cs | 14 +-- sample/MinApi/Properties/launchSettings.json | 2 +- sample/MinApi/SampleMinimalApiSli.csproj | 3 +- sample/WebApi/Program.cs | 13 +-- sample/WebApi/Properties/launchSettings.json | 2 +- sample/WebApi/SampleWebApplicationSLI.csproj | 3 +- .../WebApiVersioned/AddApiVersionMetadata.cs | 69 -------------- .../ConfigureSwaggerDefaultOptions.cs | 89 ------------------- sample/WebApiVersioned/Program.cs | 39 ++------ .../Properties/launchSettings.json | 1 + .../SampleVersionedWebApplicationSLI.csproj | 3 +- 13 files changed, 25 insertions(+), 217 deletions(-) delete mode 100644 sample/WebApiVersioned/AddApiVersionMetadata.cs delete mode 100644 sample/WebApiVersioned/ConfigureSwaggerDefaultOptions.cs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6124173..d29e994 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -3,4 +3,4 @@ ## Package Constraints - **FluentAssertions**: Do not upgrade beyond major version 7.x due to a licensing change in version 8+. -- **Swashbuckle.AspNetCore**: Do not upgrade beyond version 6.x due to breaking changes in Microsoft.OpenApi v2. + diff --git a/Directory.Packages.props b/Directory.Packages.props index 2e8c89d..36e7c45 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ - + diff --git a/sample/MinApi/Program.cs b/sample/MinApi/Program.cs index f58e5de..6a50fe1 100644 --- a/sample/MinApi/Program.cs +++ b/sample/MinApi/Program.cs @@ -1,6 +1,7 @@ using Azure.Core; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; +using Scalar.AspNetCore; using SampleMinimalApiSli; using ServiceLevelIndicators; @@ -13,14 +14,7 @@ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(options => -{ - - var fileName = typeof(Program).Assembly.GetName().Name + ".xml"; - var filePath = Path.Combine(AppContext.BaseDirectory, fileName); - options.IncludeXmlComments(filePath); -}); +builder.Services.AddOpenApi(); // Build a resource configuration action to set service information. @@ -65,8 +59,8 @@ .AddServiceLevelIndicator("background_work"); app.UseUserRoute(); -app.UseSwagger(); -app.UseSwaggerUI(); +app.MapOpenApi(); +app.MapScalarApiReference(); app.UseHttpsRedirection(); app.UseServiceLevelIndicator(); app.Run(); diff --git a/sample/MinApi/Properties/launchSettings.json b/sample/MinApi/Properties/launchSettings.json index 46e4f27..a940696 100644 --- a/sample/MinApi/Properties/launchSettings.json +++ b/sample/MinApi/Properties/launchSettings.json @@ -3,7 +3,7 @@ "SampleMinimalApiSli": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "swagger", + "launchUrl": "scalar/v1", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/sample/MinApi/SampleMinimalApiSli.csproj b/sample/MinApi/SampleMinimalApiSli.csproj index ad17a2b..8469fa6 100644 --- a/sample/MinApi/SampleMinimalApiSli.csproj +++ b/sample/MinApi/SampleMinimalApiSli.csproj @@ -7,7 +7,8 @@ - + + diff --git a/sample/WebApi/Program.cs b/sample/WebApi/Program.cs index 7eb2fdc..6e54c23 100644 --- a/sample/WebApi/Program.cs +++ b/sample/WebApi/Program.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Options; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; +using Scalar.AspNetCore; using SampleWebApplicationSLI; using ServiceLevelIndicators; @@ -13,13 +14,7 @@ builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(options => -{ - - var fileName = typeof(Program).Assembly.GetName().Name + ".xml"; - var filePath = Path.Combine(AppContext.BaseDirectory, fileName); - options.IncludeXmlComments(filePath); -}); +builder.Services.AddOpenApi(); builder.Services.AddProblemDetails(); // Build a resource configuration action to set service information. @@ -47,8 +42,8 @@ var app = builder.Build(); -app.UseSwagger(); -app.UseSwaggerUI(); +app.MapOpenApi(); +app.MapScalarApiReference(); app.UseHttpsRedirection(); app.UseServiceLevelIndicator(); app.UseAuthorization(); diff --git a/sample/WebApi/Properties/launchSettings.json b/sample/WebApi/Properties/launchSettings.json index 51932bc..0649eac 100644 --- a/sample/WebApi/Properties/launchSettings.json +++ b/sample/WebApi/Properties/launchSettings.json @@ -3,7 +3,7 @@ "SampleWebApplicationSLI": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "swagger", + "launchUrl": "scalar/v1", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/sample/WebApi/SampleWebApplicationSLI.csproj b/sample/WebApi/SampleWebApplicationSLI.csproj index ad17a2b..8469fa6 100644 --- a/sample/WebApi/SampleWebApplicationSLI.csproj +++ b/sample/WebApi/SampleWebApplicationSLI.csproj @@ -7,7 +7,8 @@ - + + diff --git a/sample/WebApiVersioned/AddApiVersionMetadata.cs b/sample/WebApiVersioned/AddApiVersionMetadata.cs deleted file mode 100644 index f8e7a7d..0000000 --- a/sample/WebApiVersioned/AddApiVersionMetadata.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace SampleVersionedWebApplicationSLI; - -using System.Globalization; -using System.Text.Json; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -/// -/// 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 AddApiVersionMetadata : 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(CultureInfo.InvariantCulture); - 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 && - 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 diff --git a/sample/WebApiVersioned/ConfigureSwaggerDefaultOptions.cs b/sample/WebApiVersioned/ConfigureSwaggerDefaultOptions.cs deleted file mode 100644 index 7a3279f..0000000 --- a/sample/WebApiVersioned/ConfigureSwaggerDefaultOptions.cs +++ /dev/null @@ -1,89 +0,0 @@ -namespace SampleVersionedWebApplicationSLI; - -using System.Text; -using Asp.Versioning; -using Asp.Versioning.ApiExplorer; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -/// -/// 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 ConfigureSwaggerDefaultOptions : IConfigureOptions -{ - private readonly IApiVersionDescriptionProvider provider; - - /// - /// Initializes a new instance of the class. - /// - /// The provider used to generate Swagger documents. - public ConfigureSwaggerDefaultOptions(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 = "Best Weather forecast API", - Version = description.ApiVersion.ToString(), - Contact = new OpenApiContact() { Name = "Xavier John", Email = "xavier@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 SunsetPolicy policy) - { - if (policy.Date is DateTimeOffset when) - { - text.Append(" The API will be sunset on ") - .Append(when.Date.ToShortDateString()) - .Append('.'); - } - - if (policy.HasLinks) - { - text.AppendLine(); - - for (var i = 0; i < policy.Links.Count; i++) - { - var link = policy.Links[i]; - - if (link.Type == "text/html") - { - text.AppendLine(); - - if (link.Title.HasValue) - { - text.Append(link.Title.Value).Append(": "); - } - - text.Append(link.LinkTarget.OriginalString); - } - } - } - } - - info.Description = text.ToString(); - - return info; - } -} \ No newline at end of file diff --git a/sample/WebApiVersioned/Program.cs b/sample/WebApiVersioned/Program.cs index 742b310..35a6dfa 100644 --- a/sample/WebApiVersioned/Program.cs +++ b/sample/WebApiVersioned/Program.cs @@ -1,31 +1,15 @@ using Azure.Core; -using Microsoft.Extensions.Options; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; -using SampleVersionedWebApplicationSLI; +using Scalar.AspNetCore; using ServiceLevelIndicators; -using Swashbuckle.AspNetCore.SwaggerGen; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddTransient, ConfigureSwaggerDefaultOptions>(); -builder.Services.AddSwaggerGen( - options => - { - // add a custom operation filter which sets default values - options.OperationFilter(); - - var fileName = typeof(Program).Assembly.GetName().Name + ".xml"; - var filePath = Path.Combine(AppContext.BaseDirectory, fileName); - - // integrate XML comments - options.IncludeXmlComments(filePath); - }); +builder.Services.AddOpenApi(); builder.Services.AddApiVersioning() .AddMvc() .AddApiExplorer(); @@ -49,21 +33,10 @@ var app = builder.Build(); -app.UseSwagger(); -app.UseSwaggerUI( - options => - { - options.RoutePrefix = string.Empty; // make home page the swagger UI - 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); - } - }); +// TODO: Use .AddOpenApi() from Asp.Versioning.OpenApi with WithDocumentPerVersion() +// and AddScalarTransformers() once a stable release is available. +app.MapOpenApi(); +app.MapScalarApiReference(); // Random delay. Random rnd = new Random(); diff --git a/sample/WebApiVersioned/Properties/launchSettings.json b/sample/WebApiVersioned/Properties/launchSettings.json index 5a64df5..fb1873e 100644 --- a/sample/WebApiVersioned/Properties/launchSettings.json +++ b/sample/WebApiVersioned/Properties/launchSettings.json @@ -3,6 +3,7 @@ "SampleVersionedWebApplicationSLI": { "commandName": "Project", "launchBrowser": true, + "launchUrl": "scalar/v1", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/sample/WebApiVersioned/SampleVersionedWebApplicationSLI.csproj b/sample/WebApiVersioned/SampleVersionedWebApplicationSLI.csproj index 77707f7..b276694 100644 --- a/sample/WebApiVersioned/SampleVersionedWebApplicationSLI.csproj +++ b/sample/WebApiVersioned/SampleVersionedWebApplicationSLI.csproj @@ -9,7 +9,8 @@ - + +