From eb78e671bce544f378778ea994231fb13d45958a Mon Sep 17 00:00:00 2001 From: Xavier Date: Wed, 4 Mar 2026 19:30:36 -0800 Subject: [PATCH 1/4] 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 @@ - + + From 906e39a0e8eddb65fc7ffb65d0002b9cefc7da62 Mon Sep 17 00:00:00 2001 From: Xavier Date: Sat, 14 Mar 2026 13:32:35 -0700 Subject: [PATCH 2/4] Update package versions for Asp.Versioning.Http, Asp.Versioning.Mvc, and Asp.Versioning.Mvc.ApiExplorer to 10.0.0-preview.2 --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 36e7c45..fa74aee 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,9 +1,9 @@ - - - + + + From d92f7710b580ab4f54e40012cabc14291f0e346b Mon Sep 17 00:00:00 2001 From: Xavier Date: Sat, 14 Mar 2026 13:41:14 -0700 Subject: [PATCH 3/4] Refactor README and usage reference to update service registration for ServiceLevelIndicator with Configure method and improve section headings for clarity --- README.md | 33 +++++++++++----------------- ServiceLevelIndicators/src/README.md | 8 +++++-- docs/usage-reference.md | 8 +++++-- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index e8cc652..734ffa6 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ For API Versioning support: dotnet add package ServiceLevelIndicators.Asp.ApiVersioning ``` -## Usage for Web API MVC +## Usage for ASP.NET Core MVC 1. Register SLI with open telemetry by calling `AddServiceLevelIndicatorInstrumentation`. @@ -142,7 +142,7 @@ dotnet add package ServiceLevelIndicators.Asp.ApiVersioning app.UseServiceLevelIndicator(); ``` -## Usage for Minimal API +## Usage for Minimal APIs 1. Register SLI with open telemetry by calling `AddServiceLevelIndicatorInstrumentation`. @@ -186,7 +186,7 @@ dotnet add package ServiceLevelIndicators.Asp.ApiVersioning .AddServiceLevelIndicator(); ``` -### Usage for background jobs +## Usage for Background Jobs You can measure a block of code by wrapping it in a `using` clause of `MeasuredOperation`. @@ -201,14 +201,16 @@ async Task MeasureCodeBlock(ServiceLevelIndicator serviceLevelIndicator) } ``` -### Customizations +## Operational Guidance -### Cardinality guidance +### Cardinality Guidance Metric dimensions should stay bounded. `CustomerResourceId` and values captured with `[Measure]` are useful when they represent a stable tenant, customer group, plan, environment, or region, but they become expensive if you feed them raw per-user or highly variable values. Prefer values with a controlled set of outcomes. Avoid using email addresses, request IDs, timestamps, or unconstrained free text unless your metrics backend is explicitly designed for high-cardinality telemetry. +## ASP.NET Core Customizations + Once the Prerequisites are done, all controllers will emit SLI information. The default operation name is in the format <HTTP Method> <Controller>/<Action>. eg GET WeatherForecast/Action1 @@ -316,25 +318,16 @@ eg GET WeatherForecast/Action1 - To prevent automatically emitting SLI information on all controllers, set the option, ``` csharp - ServiceLevelIndicatorOptions.AutomaticallyEmitted = false; + builder.Services.AddServiceLevelIndicator(options => + { + options.AutomaticallyEmitted = false; + }) + .AddMvc(); ``` In this case, add the attribute `[ServiceLevelIndicator]` on the controllers that should emit SLI. -- To measure a process, run it within a `using StartMeasuring` block. - - Example: - - ```csharp - public void StoreItem(MyDomainEvent domainEvent) - { - var attribute = new KeyValuePair("Event", domainEvent.GetType().Name); - using var measuredOperation = _serviceLevelIndicator.StartMeasuring("StoreItem", attribute); - DoTheWork(); - } - ``` - -### Sample +## Sample Try out the sample weather forecast Web API. diff --git a/ServiceLevelIndicators/src/README.md b/ServiceLevelIndicators/src/README.md index a6fcf21..70a688b 100644 --- a/ServiceLevelIndicators/src/README.md +++ b/ServiceLevelIndicators/src/README.md @@ -43,22 +43,26 @@ builder.Services.AddOpenTelemetry() metrics.AddOtlpExporter(); }); -builder.Services.AddServiceLevelIndicator(options => +builder.Services.Configure(options => { options.Meter = sliMeter; options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3"); options.CustomerResourceId = "my-customer"; }); + +builder.Services.AddSingleton(); ``` ### 2. Configure options ```csharp -builder.Services.AddServiceLevelIndicator(options => +builder.Services.Configure(options => { options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3"); options.CustomerResourceId = "my-customer"; }); + +builder.Services.AddSingleton(); ``` ### 3. Measure operations diff --git a/docs/usage-reference.md b/docs/usage-reference.md index b32f9e2..72c0896 100644 --- a/docs/usage-reference.md +++ b/docs/usage-reference.md @@ -48,11 +48,13 @@ builder.Services.AddOpenTelemetry() Register the service: ```csharp -builder.Services.AddServiceLevelIndicator(options => +builder.Services.Configure(options => { options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3"); options.CustomerResourceId = "tenant-a"; }); + +builder.Services.AddSingleton(); ``` Measure work: @@ -89,12 +91,14 @@ builder.Services.AddOpenTelemetry() metrics.AddOtlpExporter(); }); -builder.Services.AddServiceLevelIndicator(options => +builder.Services.Configure(options => { options.Meter = sliMeter; options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3"); options.CustomerResourceId = "tenant-a"; }); + +builder.Services.AddSingleton(); ``` Available registration overloads: From 9d9b5e860c4052fa3d493e81a8b8ef11dece3a38 Mon Sep 17 00:00:00 2001 From: Xavier Date: Sat, 14 Mar 2026 13:48:30 -0700 Subject: [PATCH 4/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- sample/WebApi/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample/WebApi/Program.cs b/sample/WebApi/Program.cs index 6e54c23..0a0795b 100644 --- a/sample/WebApi/Program.cs +++ b/sample/WebApi/Program.cs @@ -12,7 +12,7 @@ // Add services to the container. builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +// Learn more about configuring the built-in OpenAPI + Scalar setup in ASP.NET Core builder.Services.AddEndpointsApiExplorer(); builder.Services.AddOpenApi(); builder.Services.AddProblemDetails();