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 @@
+