diff --git a/.spectral.yml b/.spectral.yml index 45e3cef90..485989c47 100644 --- a/.spectral.yml +++ b/.spectral.yml @@ -1,6 +1,7 @@ extends: spectral:oas rules: info-contact: off + oas3-api-servers: off success-response: description: All operations should have a success response. diff --git a/Directory.Packages.props b/Directory.Packages.props index 40c1c94b4..d05d03343 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ 13.2.0-preview.1.26170.3 2.76.0 7.3.1 - 8.1.0 + 10.0.0-preview.2 @@ -26,6 +26,7 @@ + @@ -93,4 +94,4 @@ - \ No newline at end of file + diff --git a/src/Catalog.API/Catalog.API.http b/src/Catalog.API/Catalog.API.http index 46b7fe915..28e5d89cf 100644 --- a/src/Catalog.API/Catalog.API.http +++ b/src/Catalog.API/Catalog.API.http @@ -1,31 +1,45 @@ -@Catalog.API_HostAddress = http://localhost:5222 +@HostAddress = http://localhost:5222 @ApiVersion = 1.0 -GET {{Catalog.API_HostAddress}}/openapi/v1.json +GET {{HostAddress}}/openapi/v1.json ### -GET {{Catalog.API_HostAddress}}/api/catalog/items?api-version={{ApiVersion}} +GET {{HostAddress}}/openapi/v2.json ### -GET {{Catalog.API_HostAddress}}/api/catalog/items/type/1/brand/2?api-version={{ApiVersion}} +# Scalar: http://localhost:5222/scalar/v1 + +### + +# api-version is required, so this request will fail + +GET {{HostAddress}}/api/catalog/items + +### + +GET {{HostAddress}}/api/catalog/items?api-version={{ApiVersion}} + +### + +GET {{HostAddress}}/api/catalog/items/type/1/brand/2?api-version={{ApiVersion}} ### # A request with an unknown API version returns a 400 ProblemDetails response -GET {{Catalog.API_HostAddress}}/api/catalog/items/463/pic?api-version=99 +GET {{HostAddress}}/api/catalog/items/463/pic?api-version=99 ### # A request with an unknown item id returns a 404 NotFound with empty response body -GET {{Catalog.API_HostAddress}}/api/catalog/items/463/pic?api-version={{ApiVersion}} +GET {{HostAddress}}/api/catalog/items/463/pic?api-version={{ApiVersion}} ### -PUT {{Catalog.API_HostAddress}}/api/catalog/items?api-version={{ApiVersion}} +PUT {{HostAddress}}/api/catalog/items?api-version={{ApiVersion}} content-type: application/json { diff --git a/src/Catalog.API/Program.cs b/src/Catalog.API/Program.cs index 179de5bfa..391824fa8 100644 --- a/src/Catalog.API/Program.cs +++ b/src/Catalog.API/Program.cs @@ -1,13 +1,14 @@ -using Asp.Versioning.Builder; -using System.Reflection; - -var builder = WebApplication.CreateBuilder(args); +var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); builder.AddApplicationServices(); builder.Services.AddProblemDetails(); -var withApiVersioning = builder.Services.AddApiVersioning(); +var withApiVersioning = builder.Services.AddApiVersioning(options => +{ + // Include "api-supported-versions" and "api-deprecated-versions" headers in all responses + options.ReportApiVersions = true; +}); builder.AddDefaultOpenApi(withApiVersioning); diff --git a/src/Ordering.API/Program.cs b/src/Ordering.API/Program.cs index 1d167be92..b51c0e976 100644 --- a/src/Ordering.API/Program.cs +++ b/src/Ordering.API/Program.cs @@ -4,7 +4,11 @@ builder.AddApplicationServices(); builder.Services.AddProblemDetails(); -var withApiVersioning = builder.Services.AddApiVersioning(); +var withApiVersioning = builder.Services.AddApiVersioning(options => +{ + // Include "api-supported-versions" and "api-deprecated-versions" headers in all responses + options.ReportApiVersions = true; +}); builder.AddDefaultOpenApi(withApiVersioning); diff --git a/src/Webhooks.API/Program.cs b/src/Webhooks.API/Program.cs index 74483d881..b69a1ddd5 100644 --- a/src/Webhooks.API/Program.cs +++ b/src/Webhooks.API/Program.cs @@ -3,7 +3,11 @@ builder.AddServiceDefaults(); builder.AddApplicationServices(); -var withApiVersioning = builder.Services.AddApiVersioning(); +var withApiVersioning = builder.Services.AddApiVersioning(options => +{ + // Include "api-supported-versions" and "api-deprecated-versions" headers in all responses + options.ReportApiVersions = true; +}); builder.AddDefaultOpenApi(withApiVersioning); diff --git a/src/eShop.ServiceDefaults/OpenApi.Extensions.cs b/src/eShop.ServiceDefaults/OpenApi.Extensions.cs index 8e939142e..b94638b7e 100644 --- a/src/eShop.ServiceDefaults/OpenApi.Extensions.cs +++ b/src/eShop.ServiceDefaults/OpenApi.Extensions.cs @@ -21,16 +21,24 @@ public static IApplicationBuilder UseDefaultOpenApi(this WebApplication app) return app; } - app.MapOpenApi(); + app.MapOpenApi().WithDocumentPerVersion(); if (app.Environment.IsDevelopment()) { + var descriptions = app.DescribeApiVersions(); + var defaultDocument = descriptions.Count > 0 ? descriptions[^1].GroupName : "v1"; + app.MapScalarApiReference(options => { // Disable default fonts to avoid download unnecessary fonts options.DefaultFonts = false; + + foreach (var description in descriptions) + { + options.AddDocument(description.GroupName, description.GroupName, isDefault: description.GroupName == defaultDocument); + } }); - app.MapGet("/", () => Results.Redirect("/scalar/v1")).ExcludeFromDescription(); + app.MapGet("/", () => Results.Redirect($"/scalar/{defaultDocument}")).ExcludeFromDescription(); } return app; @@ -57,19 +65,21 @@ public static IHostApplicationBuilder AddDefaultOpenApi( { // the default format will just be ApiVersion.ToString(); for example, 1.0. // this will format the version as "'v'major[.minor][-status]" - var versioned = apiVersioning.AddApiExplorer(options => options.GroupNameFormat = "'v'VVV"); - string[] versions = ["v1", "v2"]; - foreach (var description in versions) - { - builder.Services.AddOpenApi(description, options => + apiVersioning.AddApiExplorer(options => + { + options.GroupNameFormat = "'v'VVV"; + options.DefaultApiVersionParameterDescription = "The API version, in the format 'major.minor'."; + }) + .AddOpenApi(options => { - options.ApplyApiVersionInfo(openApi.GetRequiredValue("Document:Title"), openApi.GetRequiredValue("Document:Description")); - options.ApplyAuthorizationChecks([.. scopes.Keys]); - options.ApplySecuritySchemeDefinitions(); - options.ApplyOperationDeprecatedStatus(); - options.ApplyApiVersionDescription(); + var document = options.Document; + + document.ApplyApiVersionInfo(openApi.GetRequiredValue("Document:Title"), openApi.GetRequiredValue("Document:Description")); + document.ApplyAuthorizationChecks([.. scopes.Keys]); + document.ApplySecuritySchemeDefinitions(); + document.ApplyOperationDeprecatedStatus(); + document.ApplyApiVersionDescription(); }); - } } return builder; diff --git a/src/eShop.ServiceDefaults/OpenApiOptionsExtensions.cs b/src/eShop.ServiceDefaults/OpenApiOptionsExtensions.cs index 981fdeaf6..4991a6cc7 100644 --- a/src/eShop.ServiceDefaults/OpenApiOptionsExtensions.cs +++ b/src/eShop.ServiceDefaults/OpenApiOptionsExtensions.cs @@ -139,8 +139,10 @@ public static OpenApiOptions ApplyOperationDeprecatedStatus(this OpenApiOptions { options.AddOperationTransformer((operation, context, cancellationToken) => { - var apiDescription = context.Description; - operation.Deprecated |= apiDescription.IsDeprecated(); + operation.Deprecated = operation.Deprecated || context.Description.ActionDescriptor.EndpointMetadata + .OfType() + .Any(); + return Task.CompletedTask; }); return options; @@ -150,22 +152,12 @@ public static OpenApiOptions ApplyApiVersionDescription(this OpenApiOptions opti { options.AddOperationTransformer((operation, context, cancellationToken) => { - // Find parameter named "api-version" and add a description to it + // Add an example for the API version parameter and remove the default value var apiVersionParameter = operation.Parameters?.FirstOrDefault(p => p.Name == "api-version"); - if (apiVersionParameter is not null) + if (apiVersionParameter?.Schema is OpenApiSchema targetSchema) { - apiVersionParameter.Description = "The API version, in the format 'major.minor'."; - if (apiVersionParameter.Schema is OpenApiSchema targetSchema) - { - switch (context.DocumentName) { - case "v1": - targetSchema.Example = JsonNode.Parse("\"1.0\""); - break; - case "v2": - targetSchema.Example = JsonNode.Parse("\"2.0\""); - break; - } - } + targetSchema.Example = targetSchema.Default; + targetSchema.Default = null; } return Task.CompletedTask; }); @@ -199,7 +191,7 @@ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerC } }; document.Components ??= new(); - document.Components.SecuritySchemes ??= new Dictionary(); + document.Components.SecuritySchemes ??= new Dictionary(); document.Components.SecuritySchemes.Add("oauth2", securityScheme); return Task.CompletedTask; } diff --git a/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj b/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj index aa4b394ae..228197acf 100644 --- a/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj +++ b/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj @@ -11,6 +11,7 @@ +