From eae96c6a364ffd976fa684faa925d55931328b06 Mon Sep 17 00:00:00 2001 From: Matthew Minke Date: Wed, 30 Jul 2025 12:19:25 -0600 Subject: [PATCH 1/2] migrated the project to using convention instead of attributes. --- src/AspNetCore.SampleOpenApi.csproj | 8 +- src/AspNetCore.SampleOpenApi_v1.0.json | 216 --------------- src/AspNetCore.SampleOpenApi_v2.0.json | 257 ------------------ src/Controllers/{ => v1_0}/ErrorController.cs | 0 .../{ => v1_0}/ErrorDemoController.cs | 6 +- .../{ => v1_0}/WeatherForecastController.cs | 7 +- src/ServiceCollectionExtensions.cs | 59 ++-- src/packages.lock.json | 18 +- 8 files changed, 57 insertions(+), 514 deletions(-) delete mode 100644 src/AspNetCore.SampleOpenApi_v1.0.json delete mode 100644 src/AspNetCore.SampleOpenApi_v2.0.json rename src/Controllers/{ => v1_0}/ErrorController.cs (100%) rename src/Controllers/{ => v1_0}/ErrorDemoController.cs (90%) rename src/Controllers/{ => v1_0}/WeatherForecastController.cs (91%) diff --git a/src/AspNetCore.SampleOpenApi.csproj b/src/AspNetCore.SampleOpenApi.csproj index d87d52e..17ca164 100644 --- a/src/AspNetCore.SampleOpenApi.csproj +++ b/src/AspNetCore.SampleOpenApi.csproj @@ -17,7 +17,11 @@ - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + \ No newline at end of file diff --git a/src/AspNetCore.SampleOpenApi_v1.0.json b/src/AspNetCore.SampleOpenApi_v1.0.json deleted file mode 100644 index abf51ea..0000000 --- a/src/AspNetCore.SampleOpenApi_v1.0.json +++ /dev/null @@ -1,216 +0,0 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "OpenApi Sample API", - "description": "Sample API to test migration from Swashbuckle to AspNetCore OpenApi", - "version": "1.0" - }, - "paths": { - "/api/v1.0/errordemo/default": { - "get": { - "tags": [ - "ErrorDemo" - ], - "operationId": "GetDefaultError", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "default": { - "description": "", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/api/v1.0/errordemo/problem": { - "get": { - "tags": [ - "ErrorDemo" - ], - "operationId": "GetProblemDetails", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "409": { - "description": "Conflict", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "default": { - "description": "", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/api/v1.0/errordemo/validationproblem": { - "get": { - "tags": [ - "ErrorDemo" - ], - "operationId": "GetValidationProblemDetails", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "409": { - "description": "Conflict", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "default": { - "description": "", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/api/v1.0/forecasts": { - "get": { - "tags": [ - "WeatherForecast" - ], - "operationId": "GetWeatherForcasts", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WeatherForecast" - } - } - } - } - }, - "default": { - "description": "", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "ProblemDetails": { - "type": "object", - "properties": { - "type": { - "type": "string", - "nullable": true - }, - "title": { - "type": "string", - "nullable": true - }, - "status": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "detail": { - "type": "string", - "nullable": true - }, - "instance": { - "type": "string", - "nullable": true - } - } - }, - "WeatherForecast": { - "required": [ - "date", - "temperatureC", - "summary" - ], - "type": "object", - "properties": { - "date": { - "type": "string", - "format": "date" - }, - "temperatureC": { - "type": "integer", - "format": "int32" - }, - "temperatureF": { - "type": "integer", - "format": "int32" - }, - "summary": { - "type": "string" - } - } - } - } - }, - "tags": [ - { - "name": "ErrorDemo" - }, - { - "name": "WeatherForecast" - } - ] -} \ No newline at end of file diff --git a/src/AspNetCore.SampleOpenApi_v2.0.json b/src/AspNetCore.SampleOpenApi_v2.0.json deleted file mode 100644 index 9ba7dd7..0000000 --- a/src/AspNetCore.SampleOpenApi_v2.0.json +++ /dev/null @@ -1,257 +0,0 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "OpenApi Sample API", - "description": "Sample API to test migration from Swashbuckle to AspNetCore OpenApi", - "version": "2.0" - }, - "paths": { - "/api/v2.0/errordemo/default": { - "get": { - "tags": [ - "ErrorDemo" - ], - "operationId": "GetDefaultError", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "default": { - "description": "", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/api/v2.0/errordemo/problem": { - "get": { - "tags": [ - "ErrorDemo" - ], - "operationId": "GetProblemDetails", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "409": { - "description": "Conflict", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "default": { - "description": "", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/api/v2.0/errordemo/validationproblem": { - "get": { - "tags": [ - "ErrorDemo" - ], - "operationId": "GetValidationProblemDetails", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "409": { - "description": "Conflict", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "default": { - "description": "", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/api/v2.0/forecasts": { - "get": { - "tags": [ - "WeatherForecast" - ], - "operationId": "GetWeatherForcasts", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WeatherForecast" - } - } - } - } - }, - "default": { - "description": "", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/api/v2.0/forecasts/{date}": { - "get": { - "tags": [ - "WeatherForecast" - ], - "operationId": "GetWeatherForcast", - "parameters": [ - { - "name": "date", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "date" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WeatherForecast" - } - } - } - }, - "default": { - "description": "", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "ProblemDetails": { - "type": "object", - "properties": { - "type": { - "type": "string", - "nullable": true - }, - "title": { - "type": "string", - "nullable": true - }, - "status": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "detail": { - "type": "string", - "nullable": true - }, - "instance": { - "type": "string", - "nullable": true - } - } - }, - "WeatherForecast": { - "required": [ - "date", - "temperatureC", - "summary" - ], - "type": "object", - "properties": { - "date": { - "type": "string", - "format": "date" - }, - "temperatureC": { - "type": "integer", - "format": "int32" - }, - "temperatureF": { - "type": "integer", - "format": "int32" - }, - "summary": { - "type": "string" - } - } - } - } - }, - "tags": [ - { - "name": "ErrorDemo" - }, - { - "name": "WeatherForecast" - } - ] -} \ No newline at end of file diff --git a/src/Controllers/ErrorController.cs b/src/Controllers/v1_0/ErrorController.cs similarity index 100% rename from src/Controllers/ErrorController.cs rename to src/Controllers/v1_0/ErrorController.cs diff --git a/src/Controllers/ErrorDemoController.cs b/src/Controllers/v1_0/ErrorDemoController.cs similarity index 90% rename from src/Controllers/ErrorDemoController.cs rename to src/Controllers/v1_0/ErrorDemoController.cs index 364bb90..62afff0 100644 --- a/src/Controllers/ErrorDemoController.cs +++ b/src/Controllers/v1_0/ErrorDemoController.cs @@ -4,11 +4,9 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using SampleOpenApi.Controllers; -namespace AspNetCore.SampleOpenApi.Controllers; +namespace AspNetCore.SampleOpenApi.Controllers.v1_0; -[ApiVersion("1.0")] -[ApiVersion("2.0")] -[Route("api/v{version:apiVersion}/errordemo")] +[Route("api/errordemo")] public class ErrorDemoController : ApiControllerBase { [HttpGet("default", Name = nameof(GetDefaultError))] diff --git a/src/Controllers/WeatherForecastController.cs b/src/Controllers/v1_0/WeatherForecastController.cs similarity index 91% rename from src/Controllers/WeatherForecastController.cs rename to src/Controllers/v1_0/WeatherForecastController.cs index 6c86738..8832600 100644 --- a/src/Controllers/WeatherForecastController.cs +++ b/src/Controllers/v1_0/WeatherForecastController.cs @@ -4,9 +4,9 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using SampleOpenApi.Controllers; -namespace AspNetCore.SampleOpenApi.Controllers; +namespace AspNetCore.SampleOpenApi.Controllers.v1_0; -[Route("api/v{version:apiVersion}/forecasts")] +[Route("api/forecasts")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "")] public class WeatherForecastController : ApiControllerBase { @@ -18,8 +18,6 @@ public class WeatherForecastController : ApiControllerBase /// Get weather forcasts /// /// The collection of s - [ApiVersion("1.0")] - [ApiVersion("2.0")] [HttpGet(Name = nameof(GetWeatherForcasts))] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [ProducesDefaultResponseType] @@ -39,7 +37,6 @@ public IEnumerable GetWeatherForcasts() /// /// The the weather forecast date /// The - [ApiVersion("2.0")] [HttpGet("{date}", Name = nameof(GetWeatherForcast))] [ProducesResponseType(typeof(WeatherForecast), StatusCodes.Status200OK)] [ProducesDefaultResponseType] diff --git a/src/ServiceCollectionExtensions.cs b/src/ServiceCollectionExtensions.cs index f38361c..d1e24dc 100644 --- a/src/ServiceCollectionExtensions.cs +++ b/src/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using Asp.Versioning; +using Asp.Versioning.Conventions; using AspNetCore.SampleOpenApi; using AspNetCore.SampleOpenApi.Transformers; using Microsoft.AspNetCore.Mvc; @@ -25,28 +26,36 @@ public static IServiceCollection AddCustomOpenApi(this IServiceCollection servic if (options.Versions?.Any() ?? false) { - /* var versioningBuilder = */ services - .AddApiVersioning(o => - { - o.AssumeDefaultVersionWhenUnspecified = options.DefaultVersionAssumedWhenUnspecified; - - if (options.DefaultVersion == null) - { - o.ApiVersionSelector = new CurrentImplementationApiVersionSelector(o); - } - else - { - var (major, minor) = options.DefaultVersion.Value; - o.DefaultApiVersion = new ApiVersion(major, minor); - } - }) - .AddApiExplorer(o => - { - o.GroupNameFormat = "'v'V'.'v"; - o.SubstituteApiVersionInUrl = true; - o.SubstitutionFormat = "V'.'v"; - }); - // .AddMvc(o => { //o.Conventions }); + /* var versioningBuilder = */ + services + .AddApiVersioning(o => + { + o.ApiVersionReader = ApiVersionReader.Combine( + new HeaderApiVersionReader("api-version"), + new QueryStringApiVersionReader("api-version") + ); + o.AssumeDefaultVersionWhenUnspecified = options.DefaultVersionAssumedWhenUnspecified; + o.ReportApiVersions = true; + //if (options.DefaultVersion == null) + //{ + // o.ApiVersionSelector = new CurrentImplementationApiVersionSelector(o); + //} + //else + //{ + // var (major, minor) = options.DefaultVersion.Value; + // o.DefaultApiVersion = new ApiVersion(major, minor); + //} + }) + .AddApiExplorer(o => + { + o.GroupNameFormat = "'v'V'.'v"; + //o.SubstituteApiVersionInUrl = true; + //o.SubstitutionFormat = "V'.'v"; + }) + .AddMvc(o => + { + o.Conventions.Add(new VersionByNamespaceConvention()); + }); @@ -76,7 +85,8 @@ public static IServiceCollection AddCustomControllers(this IServiceCollection se .AddJsonOptions(o => o.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase ) - .AddMvcOptions(o => { + .AddMvcOptions(o => + { // Remove redundant output formatters ("text/plain" and "text/json") o.RemoveRedundantOutputFormatters(); @@ -89,7 +99,8 @@ public static IServiceCollection AddCustomControllers(this IServiceCollection se return services; } - private static MvcOptions RemoveRedundantOutputFormatters(this MvcOptions mvcOptions) { + private static MvcOptions RemoveRedundantOutputFormatters(this MvcOptions mvcOptions) + { // Remove string output formatter mvcOptions .OutputFormatters diff --git a/src/packages.lock.json b/src/packages.lock.json index 799fde6..0b5c03a 100644 --- a/src/packages.lock.json +++ b/src/packages.lock.json @@ -13,18 +13,24 @@ }, "Microsoft.AspNetCore.OpenApi": { "type": "Direct", - "requested": "[9.0.*, )", - "resolved": "9.0.0", - "contentHash": "FqUK5j1EOPNuFT7IafltZQ3cakqhSwVzH5ZW1MhZDe4pPXs9sJ2M5jom1Omsu+mwF2tNKKlRAzLRHQTZzbd+6Q==", + "requested": "[9.0.7, )", + "resolved": "9.0.7", + "contentHash": "8aG0mkgmA38IDJ0ca5HIpdexKjHXIh0z1kIdw5WyM6CrD4+CEt97UgSwBBBCHG6QQKV0hj2mfkwtEcqrJBcu8g==", "dependencies": { "Microsoft.OpenApi": "1.6.17" } }, "Microsoft.Extensions.ApiDescription.Server": { "type": "Direct", - "requested": "[9.0.*, )", - "resolved": "9.0.0", - "contentHash": "1Kzzf7pRey40VaUkHN9/uWxrKVkLu2AQjt+GVeeKLLpiEHAJ1xZRsLSh4ZZYEnyS7Kt2OBOPmsXNdU+wbcOl5w==" + "requested": "[9.0.7, )", + "resolved": "9.0.7", + "contentHash": "51aeqSIFKvZgXhLwMBP5VFySJoSTJhgMhMUsH42GNSOZ8SJl7013b6c4IM8xOxVF2SHUSxm7dENrDvhdJv9bvA==" + }, + "Scalar.AspNetCore": { + "type": "Direct", + "requested": "[2.6.5, )", + "resolved": "2.6.5", + "contentHash": "9HfrTG0yTZoTFrqtwzKb0bcqH6nS1RJEtIsaumyoWWOuAa660zjvsOJPSgmYE/XzBsdPu0Y+NmA9gSDiTnrWbQ==" }, "Asp.Versioning.Abstractions": { "type": "Transitive", From a2a2c94e7cf429e60f97fd10d3e785a417bcfaff Mon Sep 17 00:00:00 2001 From: Matthew Minke Date: Wed, 30 Jul 2025 16:05:50 -0600 Subject: [PATCH 2/2] Add WeatherForecastController and OpenAPI documentation for v2.0 --- src/AspNetCore.SampleOpenApi.csproj | 2 +- src/AspNetCore.SampleOpenApi_v1.0.json | 340 ++++++++++++++++++ src/AspNetCore.SampleOpenApi_v2.0.json | 176 +++++++++ .../v2_0/WeatherForecastController.cs | 52 +++ src/Program.cs | 1 + src/WebApplicationExtensions.cs | 21 ++ 6 files changed, 591 insertions(+), 1 deletion(-) create mode 100644 src/AspNetCore.SampleOpenApi_v1.0.json create mode 100644 src/AspNetCore.SampleOpenApi_v2.0.json create mode 100644 src/Controllers/v2_0/WeatherForecastController.cs diff --git a/src/AspNetCore.SampleOpenApi.csproj b/src/AspNetCore.SampleOpenApi.csproj index 17ca164..9c95368 100644 --- a/src/AspNetCore.SampleOpenApi.csproj +++ b/src/AspNetCore.SampleOpenApi.csproj @@ -1,4 +1,4 @@ - + net9.0 diff --git a/src/AspNetCore.SampleOpenApi_v1.0.json b/src/AspNetCore.SampleOpenApi_v1.0.json new file mode 100644 index 0000000..189c8df --- /dev/null +++ b/src/AspNetCore.SampleOpenApi_v1.0.json @@ -0,0 +1,340 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApi Sample API", + "description": "Sample API to test migration from Swashbuckle to AspNetCore OpenApi", + "version": "1.0" + }, + "paths": { + "/api/errordemo/default": { + "get": { + "tags": [ + "ErrorDemo" + ], + "operationId": "GetDefaultError", + "parameters": [ + { + "name": "api-version", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "api-version", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "default": { + "description": "", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/errordemo/problem": { + "get": { + "tags": [ + "ErrorDemo" + ], + "operationId": "GetProblemDetails", + "parameters": [ + { + "name": "api-version", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "api-version", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "default": { + "description": "", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/errordemo/validationproblem": { + "get": { + "tags": [ + "ErrorDemo" + ], + "operationId": "GetValidationProblemDetails", + "parameters": [ + { + "name": "api-version", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "api-version", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "default": { + "description": "", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/forecasts": { + "get": { + "tags": [ + "WeatherForecast" + ], + "operationId": "GetWeatherForcasts", + "parameters": [ + { + "name": "api-version", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "api-version", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WeatherForecast" + } + } + } + } + }, + "default": { + "description": "", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/forecasts/{date}": { + "get": { + "tags": [ + "WeatherForecast" + ], + "operationId": "GetWeatherForcast", + "parameters": [ + { + "name": "date", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "api-version", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "api-version", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WeatherForecast" + } + } + } + }, + "default": { + "description": "", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ProblemDetails": { + "type": "object", + "properties": { + "type": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "status": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "detail": { + "type": "string", + "nullable": true + }, + "instance": { + "type": "string", + "nullable": true + } + } + }, + "WeatherForecast": { + "required": [ + "date", + "temperatureC", + "summary" + ], + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date" + }, + "temperatureC": { + "type": "integer", + "format": "int32" + }, + "temperatureF": { + "type": "integer", + "format": "int32" + }, + "summary": { + "type": "string" + } + } + } + } + }, + "tags": [ + { + "name": "ErrorDemo" + }, + { + "name": "WeatherForecast" + } + ] +} \ No newline at end of file diff --git a/src/AspNetCore.SampleOpenApi_v2.0.json b/src/AspNetCore.SampleOpenApi_v2.0.json new file mode 100644 index 0000000..7f3e375 --- /dev/null +++ b/src/AspNetCore.SampleOpenApi_v2.0.json @@ -0,0 +1,176 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApi Sample API", + "description": "Sample API to test migration from Swashbuckle to AspNetCore OpenApi", + "version": "2.0" + }, + "paths": { + "/api/forecasts": { + "get": { + "tags": [ + "WeatherForecast" + ], + "operationId": "GetWeatherForcasts", + "parameters": [ + { + "name": "api-version", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "api-version", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WeatherForecast" + } + } + } + } + }, + "default": { + "description": "", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/forecasts/{date}": { + "get": { + "tags": [ + "WeatherForecast" + ], + "operationId": "GetWeatherForcast", + "parameters": [ + { + "name": "date", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "api-version", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "api-version", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WeatherForecast" + } + } + } + }, + "default": { + "description": "", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ProblemDetails": { + "type": "object", + "properties": { + "type": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "status": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "detail": { + "type": "string", + "nullable": true + }, + "instance": { + "type": "string", + "nullable": true + } + } + }, + "WeatherForecast": { + "required": [ + "date", + "temperatureC", + "summary" + ], + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date" + }, + "temperatureC": { + "type": "integer", + "format": "int32" + }, + "temperatureF": { + "type": "integer", + "format": "int32" + }, + "summary": { + "type": "string" + } + } + } + } + }, + "tags": [ + { + "name": "WeatherForecast" + } + ] +} \ No newline at end of file diff --git a/src/Controllers/v2_0/WeatherForecastController.cs b/src/Controllers/v2_0/WeatherForecastController.cs new file mode 100644 index 0000000..8fe81c2 --- /dev/null +++ b/src/Controllers/v2_0/WeatherForecastController.cs @@ -0,0 +1,52 @@ +using Asp.Versioning; +using AspNetCore.SampleOpenApi.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using SampleOpenApi.Controllers; + +namespace AspNetCore.SampleOpenApi.Controllers.v2_0; + +[Route("api/forecasts")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "")] +public class WeatherForecastController : ApiControllerBase +{ + private static readonly string[] _summaries = [ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + ]; + + /// + /// Get weather forcasts + /// + /// The collection of s + [HttpGet(Name = nameof(GetWeatherForcasts))] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesDefaultResponseType] + public IEnumerable GetWeatherForcasts() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = _summaries[Random.Shared.Next(_summaries.Length)] + }) + .ToArray(); + } + + /// + /// Get weather forcast by date + /// + /// The the weather forecast date + /// The + [HttpGet("{date}", Name = nameof(GetWeatherForcast))] + [ProducesResponseType(typeof(WeatherForecast), StatusCodes.Status200OK)] + [ProducesDefaultResponseType] + public WeatherForecast GetWeatherForcast(DateOnly date) + { + return new WeatherForecast + { + Date = date, + TemperatureC = Random.Shared.Next(-20, 55), + Summary = _summaries[Random.Shared.Next(_summaries.Length)] + }; + } +} diff --git a/src/Program.cs b/src/Program.cs index 278eed9..7f22c67 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -20,6 +20,7 @@ .UseCustomExceptionHandler() .UseCustomAuthorization() .MapCustomOpenApi() + .UseCustomScalar() .MapCustomControllers(); await app.RunAsync(); diff --git a/src/WebApplicationExtensions.cs b/src/WebApplicationExtensions.cs index 349535d..e9223d4 100644 --- a/src/WebApplicationExtensions.cs +++ b/src/WebApplicationExtensions.cs @@ -1,5 +1,6 @@ using Asp.Versioning; using Asp.Versioning.ApiExplorer; +using Scalar.AspNetCore; namespace Microsoft.AspNetCore.Builder; @@ -8,6 +9,7 @@ internal static class WebApplicationExtensions public static WebApplication MapCustomOpenApi(this WebApplication app) { app.MapOpenApi("/openapi/{documentName}.json"); + //app.MapScalarApiReference(); return app; } @@ -39,4 +41,23 @@ public static WebApplication MapCustomControllers(this WebApplication app) return app; } + + public static WebApplication UseCustomScalar(this WebApplication app) + { + var versionDescriptionProvider = app.Services.GetRequiredService(); + + app.MapScalarApiReference("/documentation", options => + { + options.WithTitle("OpenApi Sample API"); + + // Add all versions to a single Scalar UI with proper version names + foreach (var description in versionDescriptionProvider.ApiVersionDescriptions) + { + var versionTitle = $"Version {description.ApiVersion}"; + options.AddDocument(description.GroupName, versionTitle, $"/openapi/{description.GroupName}.json"); + } + }); + + return app; + } }