From dac06e6bd6c3fa59935138c0ac37e154700a6e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Mon, 24 Jun 2024 21:09:54 +0200 Subject: [PATCH 1/7] Fix RequestBodyFilter for MinimalApi --- .../AnnotationsRequestBodyFilter.cs | 5 ++--- test/WebSites/WebApi/Program.cs | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsRequestBodyFilter.cs b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsRequestBodyFilter.cs index 9ec9e3536d..52eebeb492 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsRequestBodyFilter.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsRequestBodyFilter.cs @@ -39,8 +39,7 @@ private void ApplyPropertyAnnotations(OpenApiRequestBody parameter, PropertyInfo private void ApplyParamAnnotations(OpenApiRequestBody requestBody, ParameterInfo parameterInfo) { - var swaggerRequestBodyAttribute = parameterInfo.GetCustomAttributes() - .FirstOrDefault(); + var swaggerRequestBodyAttribute = parameterInfo.GetCustomAttribute(); if (swaggerRequestBodyAttribute != null) ApplySwaggerRequestBodyAttribute(requestBody, swaggerRequestBodyAttribute); @@ -55,4 +54,4 @@ private void ApplySwaggerRequestBodyAttribute(OpenApiRequestBody parameter, Swag parameter.Required = swaggerRequestBodyAttribute.RequiredFlag.Value; } } -} \ No newline at end of file +} diff --git a/test/WebSites/WebApi/Program.cs b/test/WebSites/WebApi/Program.cs index 834c8a8c49..6621e3cf18 100644 --- a/test/WebSites/WebApi/Program.cs +++ b/test/WebSites/WebApi/Program.cs @@ -40,13 +40,14 @@ app.MapPost("/fruit/{id}", ([AsParameters] CreateFruitModel model) => { return model.Fruit; -}).WithName("CreateFruit"); +}).WithName("CreateFruit") +.WithSummary("Create a fruit"); app.Run(); record struct CreateFruitModel ([FromRoute, SwaggerParameter(Description = "The id of the fruit that will be created", Required = true)] string Id, - [FromBody] Fruit Fruit); + [FromBody, SwaggerRequestBody("Description for Body")] Fruit Fruit); record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); From 4938d8ab79d0f2e09f790725e34b110890dc2af2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Thu, 27 Jun 2024 03:44:38 +0200 Subject: [PATCH 2/7] Add IntegrationTest using Verify and fix error of several [FromForm] parameters in MinimalApis --- .../SwaggerGenerator/SwaggerGenerator.cs | 31 +- ..._For_Mvc_swaggerRequestUri=v1.verified.txt | 52 + ...r_WebApi_swaggerRequestUri=v1.verified.txt | 277 +++ ....Startup_swaggerRequestUri=v1.verified.txt | 1497 +++++++++++++++++ ....Startup_swaggerRequestUri=v1.verified.txt | 54 + ....Startup_swaggerRequestUri=v1.verified.txt | 68 + ....Startup_swaggerRequestUri=v1.verified.txt | 49 + ....Startup_swaggerRequestUri=v1.verified.txt | 49 + ....Startup_swaggerRequestUri=v1.verified.txt | 149 ++ ...Startup_swaggerRequestUri=1.0.verified.txt | 75 + ...Startup_swaggerRequestUri=2.0.verified.txt | 215 +++ ....Startup_swaggerRequestUri=v1.verified.txt | 195 +++ ....Startup_swaggerRequestUri=v1.verified.txt | 188 +++ ...waggerRequestUri=v1-generated.verified.txt | 55 + ...est.TypesAreRenderedCorrectly.verified.txt | 277 +++ .../SwaggerVerifyIntegrationTest.cs | 82 + ...hbuckle.AspNetCore.IntegrationTests.csproj | 1 + .../SwaggerGenerator/SwaggerGeneratorTests.cs | 63 + ...OperationWithSeveralFromForms.verified.txt | 174 ++ .../SwaggerGeneratorVerifyTests.cs | 57 + .../WebApi/EndPoints/OpenApiEndpoints.cs | 57 + .../EndPoints/SwaggerAnnotationsEndpoints.cs | 33 + .../WebApi/EndPoints/XmlCommentsEndpoints.cs | 56 + test/WebSites/WebApi/Program.cs | 48 +- test/WebSites/WebApi/WebApi.csproj | 1 + 25 files changed, 3763 insertions(+), 40 deletions(-) create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_Mvc_swaggerRequestUri=v1.verified.txt create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.verified.txt create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=CliExample.Startup_swaggerRequestUri=v1.verified.txt create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=ConfigFromFile.Startup_swaggerRequestUri=v1.verified.txt create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=CustomUIConfig.Startup_swaggerRequestUri=v1.verified.txt create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=CustomUIIndex.Startup_swaggerRequestUri=v1.verified.txt create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=GenericControllers.Startup_swaggerRequestUri=v1.verified.txt create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=MultipleVersions.Startup_swaggerRequestUri=1.0.verified.txt create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=MultipleVersions.Startup_swaggerRequestUri=2.0.verified.txt create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=OAuth2Integration.Startup_swaggerRequestUri=v1.verified.txt create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=ReDoc.Startup_swaggerRequestUri=v1.verified.txt create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=TestFirst.Startup_swaggerRequestUri=v1-generated.verified.txt create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt create mode 100644 test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.cs create mode 100644 test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.GetSwagger_GenerateConsumesSchemas_ForProvidedOpenApiOperationWithSeveralFromForms.verified.txt create mode 100644 test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs create mode 100644 test/WebSites/WebApi/EndPoints/SwaggerAnnotationsEndpoints.cs create mode 100644 test/WebSites/WebApi/EndPoints/XmlCommentsEndpoints.cs diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index c80f8657cb..88e2da6edd 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -420,14 +420,31 @@ private OpenApiOperation GenerateOpenApiOperationFromMetadata(ApiDescription api { foreach (var content in requestContentTypes) { - var requestParameter = apiDescription.ParameterDescriptions.SingleOrDefault(desc => desc.IsFromBody() || desc.IsFromForm()); - if (requestParameter is not null) + var requestParameters = apiDescription.ParameterDescriptions.Where(desc => desc.IsFromBody() || desc.IsFromForm()); + if (requestParameters is not null) { - content.Schema = GenerateSchema( - requestParameter.ModelMetadata.ModelType, - schemaRepository, - requestParameter.PropertyInfo(), - requestParameter.ParameterInfo()); + if (requestParameters.Count() == 1) + { + content.Schema = GenerateSchema( + requestParameters.First().ModelMetadata.ModelType, + schemaRepository, + requestParameters.First().PropertyInfo(), + requestParameters.First().ParameterInfo()); + } + else + { + content.Schema = new OpenApiSchema() + { + AllOf = requestParameters.Select(s => + GenerateSchema( + s.ModelMetadata.ModelType, + schemaRepository, + s.PropertyInfo(), + s.ParameterInfo())) + .ToList() + }; + } + } } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_Mvc_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_Mvc_swaggerRequestUri=v1.verified.txt new file mode 100644 index 0000000000..37cdf55df7 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_Mvc_swaggerRequestUri=v1.verified.txt @@ -0,0 +1,52 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "MvcWithNullable", + "version": "1.0" + }, + "paths": { + "/api/Enum": { + "get": { + "tags": [ + "Enum" + ], + "parameters": [ + { + "name": "logLevel", + "in": "query", + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/LogLevel" + } + ], + "default": 4 + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "LogLevel": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "type": "integer", + "format": "int32" + } + } + } +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt new file mode 100644 index 0000000000..55573759c0 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt @@ -0,0 +1,277 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "WebApi", + "version": "v1" + }, + "paths": { + "/annotations/fruit/{id}": { + "post": { + "tags": [ + "Annotations" + ], + "summary": "CreateFruit", + "description": "Create a fruit", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The id of the fruit that will be created", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Description for Body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Fruit" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Description for response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Fruit" + } + } + } + } + } + } + }, + "/AsParameters": { + "get": { + "tags": [ + "WebApi" + ], + "parameters": [ + { + "name": "PropertyOne", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "PropertyTwo", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/WithOpenApi/weatherforecast": { + "get": { + "tags": [ + "WithOpenApi" + ], + "operationId": "GetWeatherForecast", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WeatherForecast" + } + } + } + } + } + } + } + }, + "/WithOpenApi/api/people-minimalapi": { + "post": { + "tags": [ + "WithOpenApi" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Person" + }, + { + "$ref": "#/components/schemas/Address" + } + ] + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Person" + }, + { + "$ref": "#/components/schemas/Address" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/XmlComments/Car/{id}": { + "get": { + "tags": [ + "Xml" + ], + "summary": "Returns a specific product", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The product id", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "example": 111 + } + ], + "responses": { + "200": { + "description": "A Product Id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Address": { + "type": "object", + "properties": { + "street": { + "type": "string", + "nullable": true + }, + "city": { + "type": "string", + "nullable": true + }, + "state": { + "type": "string", + "nullable": true + }, + "zipCode": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "Fruit": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Description for Schema" + }, + "Person": { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Uniquely identifies the product", + "format": "int32" + }, + "description": { + "type": "string", + "description": "Describes the product", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Represents a product" + }, + "WeatherForecast": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date" + }, + "temperatureC": { + "type": "integer", + "format": "int32" + }, + "summary": { + "type": "string", + "nullable": true + }, + "temperatureF": { + "type": "integer", + "format": "int32", + "readOnly": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.verified.txt new file mode 100644 index 0000000000..4d9e556dc5 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.verified.txt @@ -0,0 +1,1497 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Test API V1", + "description": "A sample API for testing Swashbuckle", + "termsOfService": "http://tempuri.org/terms", + "version": "v1" + }, + "paths": { + "/products": { + "post": { + "tags": [ + "CrudActions" + ], + "summary": "Creates a product", + "description": "## Heading 1\r\n\r\n POST /products\r\n {\r\n \"id\": \"123\",\r\n \"description\": \"Some product\"\r\n }", + "operationId": "CreateProduct", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + }, + "required": true, + "x-purpose": "test" + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + } + }, + "x-purpose": "test" + }, + "get": { + "tags": [ + "CrudActions" + ], + "summary": "Searches the collection of products by description key words", + "operationId": "SearchProducts", + "parameters": [ + { + "name": "kw", + "in": "query", + "description": "A list of search terms", + "schema": { + "type": "string", + "default": "foobar" + }, + "example": "hello" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + }, + "x-purpose": "test" + } + }, + "/products/{id}": { + "get": { + "tags": [ + "CrudActions" + ], + "summary": "Returns a specific product", + "operationId": "GetProduct", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The product id", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "example": 111 + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + } + }, + "x-purpose": "test" + }, + "put": { + "tags": [ + "CrudActions" + ], + "summary": "Updates all properties of a specific product", + "operationId": "UpdateProduct", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "example": 222 + } + ], + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + }, + "required": true, + "x-purpose": "test" + }, + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + }, + "patch": { + "tags": [ + "CrudActions" + ], + "summary": "Updates some properties of a specific product", + "operationId": "PatchProduct", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "example": 333 + } + ], + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { } + } + }, + "text/json": { + "schema": { + "type": "object", + "additionalProperties": { } + } + }, + "application/*+json": { + "schema": { + "type": "object", + "additionalProperties": { } + } + } + }, + "required": true, + "x-purpose": "test" + }, + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + }, + "delete": { + "tags": [ + "CrudActions" + ], + "summary": "Deletes a specific product", + "operationId": "DeleteProduct", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "example": 444 + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + } + }, + "/payments/authorize": { + "post": { + "tags": [ + "DataAnnotations" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/PaymentRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/PaymentRequest" + } + } + }, + "required": true, + "x-purpose": "test" + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + }, + "x-purpose": "test" + } + }, + "/payments/{paymentId}/cancel": { + "put": { + "tags": [ + "DataAnnotations" + ], + "parameters": [ + { + "name": "paymentId", + "in": "path", + "required": true, + "schema": { + "minLength": 6, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + } + }, + "/kittens": { + "post": { + "tags": [ + "DynamicTypes" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { } + }, + "text/json": { + "schema": { } + }, + "application/*+json": { + "schema": { } + } + }, + "x-purpose": "test" + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + }, + "x-purpose": "test" + } + }, + "/unicorns": { + "get": { + "tags": [ + "DynamicTypes" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { } + } + } + } + } + }, + "x-purpose": "test" + } + }, + "/dragons": { + "post": { + "tags": [ + "DynamicTypes" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { } + }, + "text/json": { + "schema": { } + }, + "application/*+json": { + "schema": { } + } + }, + "x-purpose": "test" + }, + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + } + }, + "/files/single": { + "post": { + "tags": [ + "Files" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + } + } + }, + "encoding": { + "file": { + "style": "form" + } + } + } + }, + "x-purpose": "test" + }, + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + } + }, + "/files/multiple": { + "post": { + "tags": [ + "Files" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "type": "string", + "format": "binary" + } + } + } + }, + "encoding": { + "files": { + "style": "form" + } + } + } + }, + "x-purpose": "test" + }, + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + } + }, + "/files/form-with-file": { + "post": { + "tags": [ + "Files" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "file": { + "type": "string", + "format": "binary" + } + } + }, + "encoding": { + "name": { + "style": "form" + }, + "file": { + "style": "form" + } + } + } + }, + "x-purpose": "test" + }, + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + } + }, + "/files/{name}": { + "get": { + "tags": [ + "Files" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "oneOf": [ + { + "type": "string", + "format": "binary" + }, + { + "type": "string", + "format": "binary" + }, + { + "type": "string", + "format": "binary" + }, + { + "type": "string", + "format": "binary" + } + ] + } + }, + "application/zip": { + "schema": { + "oneOf": [ + { + "type": "string", + "format": "binary" + }, + { + "type": "string", + "format": "binary" + }, + { + "type": "string", + "format": "binary" + }, + { + "type": "string", + "format": "binary" + } + ] + } + } + } + } + }, + "x-purpose": "test" + } + }, + "/registrations": { + "post": { + "tags": [ + "FromFormParams" + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "phoneNumbers": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + } + }, + "encoding": { + "name": { + "style": "form" + }, + "phoneNumbers": { + "style": "form" + } + } + } + }, + "x-purpose": "test" + }, + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + } + }, + "/registrationsWithIgnoreProperties": { + "post": { + "tags": [ + "FromFormParams" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "country" + ], + "type": "object", + "properties": { + "phoneNumbers": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "country": { + "type": "string", + "description": "3-letter ISO country code" + }, + "city": { + "type": "string", + "description": "Name of city", + "default": "Seattle" + } + } + }, + "encoding": { + "phoneNumbers": { + "style": "form" + }, + "country": { + "style": "form" + }, + "city": { + "style": "form" + } + } + } + }, + "x-purpose": "test" + }, + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + } + }, + "/country/validate": { + "get": { + "tags": [ + "FromHeaderParams" + ], + "parameters": [ + { + "name": "country", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + } + }, + "/addresses/validate": { + "get": { + "tags": [ + "FromQueryParams" + ], + "parameters": [ + { + "name": "country", + "in": "query", + "description": "3-letter ISO country code", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "city", + "in": "query", + "description": "Name of city", + "schema": { + "type": "string", + "default": "Seattle" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + } + }, + "/zip-codes/validate": { + "get": { + "tags": [ + "FromQueryParams" + ], + "parameters": [ + { + "name": "zipCodes", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + } + }, + "/promotions": { + "get": { + "tags": [ + "JsonAnnotations" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Promotion" + } + } + } + } + } + }, + "x-purpose": "test" + } + }, + "/shapes": { + "post": { + "tags": [ + "PolymorphicTypes" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Rectangle" + }, + { + "$ref": "#/components/schemas/Circle" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Rectangle" + }, + { + "$ref": "#/components/schemas/Circle" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Rectangle" + }, + { + "$ref": "#/components/schemas/Circle" + } + ] + } + } + }, + "x-purpose": "test" + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "text/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + }, + "x-purpose": "test" + } + }, + "/orders": { + "post": { + "tags": [ + "ResponseTypeAnnotations" + ], + "summary": "Creates an order", + "requestBody": { + "description": "", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + }, + "required": true, + "x-purpose": "test" + }, + "responses": { + "201": { + "description": "Order created", + "content": { + "application/xml": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + }, + "400": { + "description": "Order invalid", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + } + }, + "x-purpose": "test" + } + }, + "/carts": { + "post": { + "tags": [ + "SwaggerAnnotations" + ], + "operationId": "CreateCart", + "requestBody": { + "description": "The cart request body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + } + }, + "x-purpose": "test" + }, + "responses": { + "201": { + "description": "The cart was created", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + } + } + }, + "400": { + "description": "The cart data is invalid" + } + }, + "x-purpose": "test" + } + }, + "/carts/{id}": { + "get": { + "tags": [ + "SwaggerAnnotations" + ], + "externalDocs": { + "description": "External docs for CartsByIdGet", + "url": "https://tempuri.org/carts-by-id-get" + }, + "operationId": "GetCart", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The cart identifier", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + } + } + } + }, + "x-purpose": "test" + }, + "delete": { + "tags": [ + "SwaggerAnnotations" + ], + "summary": "Deletes a specific cart", + "description": "Requires admin privileges", + "operationId": "DeleteCart", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The cart identifier", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + } + } + } + }, + "x-purpose": "test" + } + }, + "/stores": { + "post": { + "tags": [ + "UnboundParams" + ], + "parameters": [ + { + "name": "id", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "location", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + }, + "x-purpose": "test" + }, + "get": { + "tags": [ + "UnboundParams" + ], + "parameters": [ + { + "name": "locations", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Store" + } + } + } + } + } + }, + "x-purpose": "test" + } + }, + "/stores/{id}": { + "get": { + "tags": [ + "UnboundParams" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Store" + } + } + } + } + }, + "x-purpose": "test" + }, + "put": { + "tags": [ + "UnboundParams" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Store" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Store" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Store" + } + } + }, + "x-purpose": "test" + }, + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + }, + "delete": { + "tags": [ + "UnboundParams" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-purpose": "test" + } + } + }, + "components": { + "schemas": { + "Cart": { + "required": [ + "Id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "The cart identifier", + "format": "int32", + "readOnly": true + }, + "cartType": { + "$ref": "#/components/schemas/CartType" + } + }, + "additionalProperties": false + }, + "CartType": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "description": "The cart type", + "format": "int32" + }, + "Circle": { + "allOf": [ + { + "$ref": "#/components/schemas/Shape" + }, + { + "type": "object", + "properties": { + "radius": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + } + ] + }, + "CreditCard": { + "required": [ + "cardNumber", + "expMonth", + "expYear" + ], + "type": "object", + "properties": { + "cardNumber": { + "minLength": 1, + "pattern": "^[3-6]?\\d{12,15}$", + "type": "string" + }, + "expMonth": { + "maximum": 12, + "minimum": 1, + "type": "integer", + "format": "int32" + }, + "expYear": { + "maximum": 99, + "minimum": 14, + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "DiscountType": { + "enum": [ + "Percentage", + "Amount" + ], + "type": "string" + }, + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "description": { + "type": "string", + "nullable": true + }, + "total": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, + "PaymentRequest": { + "required": [ + "creditCard", + "transaction" + ], + "type": "object", + "properties": { + "transaction": { + "$ref": "#/components/schemas/Transaction" + }, + "creditCard": { + "$ref": "#/components/schemas/CreditCard" + } + }, + "additionalProperties": false + }, + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Uniquely identifies the product", + "format": "int32" + }, + "description": { + "type": "string", + "description": "Describes the product", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ProductStatus" + }, + "status2": { + "$ref": "#/components/schemas/ProductStatus" + } + }, + "additionalProperties": false, + "description": "Represents a product", + "example": { + "id": 123, + "description": "foobar", + "price": 14.37 + } + }, + "ProductStatus": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "format": "int32" + }, + "Promotion": { + "type": "object", + "properties": { + "promo-code": { + "type": "string", + "nullable": true + }, + "discountType": { + "$ref": "#/components/schemas/DiscountType" + } + }, + "additionalProperties": false + }, + "Rectangle": { + "allOf": [ + { + "$ref": "#/components/schemas/Shape" + }, + { + "type": "object", + "properties": { + "height": { + "type": "integer", + "format": "int32" + }, + "width": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + } + ] + }, + "Shape": { + "required": [ + "TypeName" + ], + "type": "object", + "properties": { + "TypeName": { + "type": "string" + }, + "name": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "TypeName", + "mapping": { + "Rectangle": "#/components/schemas/Rectangle", + "Circle": "#/components/schemas/Circle" + } + } + }, + "Store": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "location": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "Transaction": { + "required": [ + "amount" + ], + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "double" + }, + "note": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "ValidationProblemDetails": { + "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 + }, + "errors": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "nullable": true + } + }, + "additionalProperties": { } + } + } + }, + "tags": [ + { + "name": "SwaggerAnnotations", + "description": "Manipulate Carts to your heart's content", + "externalDocs": { + "url": "http://www.tempuri.org" + } + } + ] +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=CliExample.Startup_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=CliExample.Startup_swaggerRequestUri=v1.verified.txt new file mode 100644 index 0000000000..9d9eceda44 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=CliExample.Startup_swaggerRequestUri=v1.verified.txt @@ -0,0 +1,54 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "CliExample", + "version": "1.0" + }, + "servers": [ + { + "url": "http://localhost:51071" + } + ], + "paths": { + "/products": { + "get": { + "tags": [ + "Products" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=ConfigFromFile.Startup_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=ConfigFromFile.Startup_swaggerRequestUri=v1.verified.txt new file mode 100644 index 0000000000..bc3d0e0d91 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=ConfigFromFile.Startup_swaggerRequestUri=v1.verified.txt @@ -0,0 +1,68 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "ConfigFromFile", + "version": "1.0" + }, + "paths": { + "/api/Products": { + "get": { + "tags": [ + "Products" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Product": { + "type": "object", + "properties": { + "foo": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "nullable": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=CustomUIConfig.Startup_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=CustomUIConfig.Startup_swaggerRequestUri=v1.verified.txt new file mode 100644 index 0000000000..5b6aea9c44 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=CustomUIConfig.Startup_swaggerRequestUri=v1.verified.txt @@ -0,0 +1,49 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "CustomUIConfig", + "version": "1.0" + }, + "paths": { + "/products": { + "get": { + "tags": [ + "Products" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=CustomUIIndex.Startup_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=CustomUIIndex.Startup_swaggerRequestUri=v1.verified.txt new file mode 100644 index 0000000000..cf5e337d2c --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=CustomUIIndex.Startup_swaggerRequestUri=v1.verified.txt @@ -0,0 +1,49 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "CustomUIIndex", + "version": "1.0" + }, + "paths": { + "/products": { + "get": { + "tags": [ + "Products" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=GenericControllers.Startup_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=GenericControllers.Startup_swaggerRequestUri=v1.verified.txt new file mode 100644 index 0000000000..eaa1e8eb16 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=GenericControllers.Startup_swaggerRequestUri=v1.verified.txt @@ -0,0 +1,149 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "1" + }, + "paths": { + "/{tennantId}/orders": { + "post": { + "tags": [ + "Orders" + ], + "summary": "Creates a resource", + "parameters": [ + { + "name": "tennantId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "text/plain": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "text/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + }, + "/{tennantId}/products": { + "post": { + "tags": [ + "Products" + ], + "summary": "Creates a resource", + "parameters": [ + { + "name": "tennantId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "text/plain": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "text/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "subtotal": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=MultipleVersions.Startup_swaggerRequestUri=1.0.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=MultipleVersions.Startup_swaggerRequestUri=1.0.verified.txt new file mode 100644 index 0000000000..7ff2150aad --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=MultipleVersions.Startup_swaggerRequestUri=1.0.verified.txt @@ -0,0 +1,75 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Sample API 1.0", + "version": "1.0" + }, + "paths": { + "/Products": { + "get": { + "tags": [ + "Products" + ], + "parameters": [ + { + "name": "api-version", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=MultipleVersions.Startup_swaggerRequestUri=2.0.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=MultipleVersions.Startup_swaggerRequestUri=2.0.verified.txt new file mode 100644 index 0000000000..292ba3ced1 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=MultipleVersions.Startup_swaggerRequestUri=2.0.verified.txt @@ -0,0 +1,215 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Sample API 2.0", + "version": "2.0" + }, + "paths": { + "/Products": { + "post": { + "tags": [ + "Products" + ], + "parameters": [ + { + "name": "api-version", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "text/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + } + }, + "get": { + "tags": [ + "Products" + ], + "parameters": [ + { + "name": "api-version", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + }, + "/Products/{id}": { + "put": { + "tags": [ + "Products" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "api-version", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Products" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "api-version", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=OAuth2Integration.Startup_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=OAuth2Integration.Startup_swaggerRequestUri=v1.verified.txt new file mode 100644 index 0000000000..e312247c58 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=OAuth2Integration.Startup_swaggerRequestUri=v1.verified.txt @@ -0,0 +1,195 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Test API V1", + "version": "v1" + }, + "servers": [ + { + "url": "/resource-server" + } + ], + "paths": { + "/products": { + "get": { + "tags": [ + "Products" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Products" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/products/{id}": { + "get": { + "tags": [ + "Products" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Product" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Products" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "serialNo": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ProductStatus" + } + }, + "additionalProperties": false + }, + "ProductStatus": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "format": "int32" + } + }, + "securitySchemes": { + "oauth2": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "/auth-server/connect/authorize", + "tokenUrl": "/auth-server/connect/token", + "scopes": { + "readAccess": "Access read operations", + "writeAccess": "Access write operations" + } + } + } + } + } + }, + "security": [ + { + "oauth2": [ + "readAccess", + "writeAccess" + ] + } + ] +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=ReDoc.Startup_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=ReDoc.Startup_swaggerRequestUri=v1.verified.txt new file mode 100644 index 0000000000..27a5d6cfb0 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=ReDoc.Startup_swaggerRequestUri=v1.verified.txt @@ -0,0 +1,188 @@ +{ + "swagger": "2.0", + "info": { + "title": "ReDoc", + "version": "1.0" + }, + "paths": { + "/products": { + "post": { + "tags": [ + "Products" + ], + "consumes": [ + "application/json", + "text/json", + "application/*+json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Product" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "format": "int32", + "type": "integer" + } + } + } + }, + "get": { + "tags": [ + "Products" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Product" + } + } + } + } + } + }, + "/products/{id}": { + "get": { + "tags": [ + "Products" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "type": "integer", + "format": "int32" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Product" + } + } + } + }, + "put": { + "tags": [ + "Products" + ], + "consumes": [ + "application/json", + "text/json", + "application/*+json" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "type": "integer", + "format": "int32" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Product" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "patch": { + "tags": [ + "Products" + ], + "consumes": [ + "application/json", + "text/json", + "application/*+json" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "type": "integer", + "format": "int32" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { } + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Products" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "type": "integer", + "format": "int32" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "definitions": { + "Product": { + "type": "object", + "properties": { + "id": { + "format": "int32", + "type": "integer" + }, + "description": { + "type": "string" + } + }, + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=TestFirst.Startup_swaggerRequestUri=v1-generated.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=TestFirst.Startup_swaggerRequestUri=v1-generated.verified.txt new file mode 100644 index 0000000000..d484e89bc5 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=TestFirst.Startup_swaggerRequestUri=v1-generated.verified.txt @@ -0,0 +1,55 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Test-first Example API (Generated)", + "version": "v1" + }, + "paths": { + "/api/users": { + "post": { + "tags": [ + "Users" + ], + "operationId": "CreateUser", + "requestBody": { + "content": { + "application/json": { + "schema": { + "required": [ + "email", + "password" + ], + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "User created", + "headers": { + "Location": { + "required": true, + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid request" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt new file mode 100644 index 0000000000..55573759c0 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt @@ -0,0 +1,277 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "WebApi", + "version": "v1" + }, + "paths": { + "/annotations/fruit/{id}": { + "post": { + "tags": [ + "Annotations" + ], + "summary": "CreateFruit", + "description": "Create a fruit", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The id of the fruit that will be created", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Description for Body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Fruit" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Description for response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Fruit" + } + } + } + } + } + } + }, + "/AsParameters": { + "get": { + "tags": [ + "WebApi" + ], + "parameters": [ + { + "name": "PropertyOne", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "PropertyTwo", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/WithOpenApi/weatherforecast": { + "get": { + "tags": [ + "WithOpenApi" + ], + "operationId": "GetWeatherForecast", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WeatherForecast" + } + } + } + } + } + } + } + }, + "/WithOpenApi/api/people-minimalapi": { + "post": { + "tags": [ + "WithOpenApi" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Person" + }, + { + "$ref": "#/components/schemas/Address" + } + ] + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Person" + }, + { + "$ref": "#/components/schemas/Address" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/XmlComments/Car/{id}": { + "get": { + "tags": [ + "Xml" + ], + "summary": "Returns a specific product", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The product id", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "example": 111 + } + ], + "responses": { + "200": { + "description": "A Product Id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Address": { + "type": "object", + "properties": { + "street": { + "type": "string", + "nullable": true + }, + "city": { + "type": "string", + "nullable": true + }, + "state": { + "type": "string", + "nullable": true + }, + "zipCode": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "Fruit": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Description for Schema" + }, + "Person": { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Uniquely identifies the product", + "format": "int32" + }, + "description": { + "type": "string", + "description": "Describes the product", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Represents a product" + }, + "WeatherForecast": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date" + }, + "temperatureC": { + "type": "integer", + "format": "int32" + }, + "summary": { + "type": "string", + "nullable": true + }, + "temperatureF": { + "type": "integer", + "format": "int32", + "readOnly": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.cs new file mode 100644 index 0000000000..d73765adbe --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.cs @@ -0,0 +1,82 @@ +using System; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using VerifyXunit; +using Xunit; +using ReDocApp = ReDoc; + +namespace Swashbuckle.AspNetCore.IntegrationTests +{ + public class SwaggerVerifyIntegrationTest + { + [Theory] + [InlineData(typeof(CliExample.Startup), "/swagger/v1/swagger_net8.0.json")] + [InlineData(typeof(ConfigFromFile.Startup), "/swagger/v1/swagger.json")] + [InlineData(typeof(CustomUIConfig.Startup), "/swagger/v1/swagger.json")] + [InlineData(typeof(CustomUIIndex.Startup), "/swagger/v1/swagger.json")] + [InlineData(typeof(GenericControllers.Startup), "/swagger/v1/swagger.json")] + [InlineData(typeof(MultipleVersions.Startup), "/swagger/1.0/swagger.json")] + [InlineData(typeof(MultipleVersions.Startup), "/swagger/2.0/swagger.json")] + [InlineData(typeof(OAuth2Integration.Startup), "/resource-server/swagger/v1/swagger.json")] + [InlineData(typeof(ReDocApp.Startup), "/swagger/v1/swagger.json")] + [InlineData(typeof(TestFirst.Startup), "/swagger/v1-generated/openapi.json")] + public async Task SwaggerEndpoint_ReturnsValidSwaggerJson( + Type startupType, + string swaggerRequestUri) + { + var testSite = new TestSite(startupType); + using var client = testSite.BuildClient(); + + using var swaggerResponse = await client.GetAsync(swaggerRequestUri); + var swagger = await swaggerResponse.Content.ReadAsStringAsync(); + await Verifier.Verify(swagger).UseParameters(startupType, GetVersion(swaggerRequestUri)); + } + +#if NET8_0_OR_GREATER + [Theory] + [InlineData("/swagger/v1/swagger.json")] + public async Task SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi( + string swaggerRequestUri) + { + var swaggerResponse = await SwaggerEndpointReturnsValidSwaggerJson(swaggerRequestUri); + await Verifier.Verify(swaggerResponse).UseParameters(GetVersion(swaggerRequestUri)); + } + + [Theory] + [InlineData("/swagger/v1/swagger.json")] + public async Task SwaggerEndpoint_ReturnsValidSwaggerJson_For_Mvc( + string swaggerRequestUri) + { + var swaggerResponse = await SwaggerEndpointReturnsValidSwaggerJson(swaggerRequestUri); + await Verifier.Verify(swaggerResponse).UseParameters(GetVersion(swaggerRequestUri)); + } + + [Fact] + public async Task TypesAreRenderedCorrectly() + { + using var application = new TestApplication(); + using var client = application.CreateDefaultClient(); + + var swaggerResponse = await SwaggerResponse(client,"/swagger/v1/swagger.json"); + await Verifier.Verify(swaggerResponse); + } + + private static async Task SwaggerEndpointReturnsValidSwaggerJson(string swaggerRequestUri) + where TEntryPoint : class + { + using var application = new TestApplication(); + using var client = application.CreateDefaultClient(); + + return await SwaggerResponse(client, swaggerRequestUri); + } +#endif + private static async Task SwaggerResponse(HttpClient client, string swaggerRequestUri) + { + using var swaggerResponse = await client.GetAsync(swaggerRequestUri); + var contentStream = await swaggerResponse.Content.ReadAsStringAsync(); + return contentStream; + } + private static string GetVersion(string swaggerUi) => Regex.Match(swaggerUi, "/\\w+/([\\w+\\d+.-]+)/").Groups[1].Value; + } +} diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/Swashbuckle.AspNetCore.IntegrationTests.csproj b/test/Swashbuckle.AspNetCore.IntegrationTests/Swashbuckle.AspNetCore.IntegrationTests.csproj index d4e5ac45f8..8390f7df37 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/Swashbuckle.AspNetCore.IntegrationTests.csproj +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/Swashbuckle.AspNetCore.IntegrationTests.csproj @@ -32,6 +32,7 @@ + diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs index 5cb20af5a2..d107c787cf 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs @@ -2035,6 +2035,69 @@ public void GetSwagger_Copies_Description_From_GeneratedSchema() Assert.Equal(document.Components.Schemas[nameof(IntEnum)].Description, operation.Parameters[1].Description); } + [Fact] + public void GetSwagger_GenerateConsumesSchemas_ForProvidedOpenApiOperationWithSeveralFromForms() + { + var methodInfo = typeof(FakeController).GetMethod(nameof(FakeController.ActionWithConsumesAttribute)); + var actionDescriptor = new ActionDescriptor + { + EndpointMetadata = + [ + new OpenApiOperation + { + OperationId = "OperationIdSetInMetadata", + RequestBody = new() + { + Content = new Dictionary() + { + ["application/someMediaType"] = new() + } + } + } + ], + RouteValues = new Dictionary + { + ["controller"] = methodInfo.DeclaringType.Name.Replace("Controller", string.Empty) + } + }; + var subject = Subject( + apiDescriptions: + [ + ApiDescriptionFactory.Create( + actionDescriptor, + methodInfo, + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + parameterDescriptions: + [ + new ApiParameterDescription() + { + Name = "param", + Source = BindingSource.Form, + ModelMetadata = ModelMetadataFactory.CreateForType(typeof(TestDto)) + }, + new ApiParameterDescription() + { + Name = "param2", + Source = BindingSource.Form, + ModelMetadata = ModelMetadataFactory.CreateForType(typeof(TypeWithDefaultAttributeOnEnum)) + } + ]), + ] + ); + + var document = subject.GetSwagger("v1"); + + Assert.Equal("OperationIdSetInMetadata", document.Paths["/resource"].Operations[OperationType.Post].OperationId); + var content = Assert.Single(document.Paths["/resource"].Operations[OperationType.Post].RequestBody.Content); + Assert.Equal("application/someMediaType", content.Key); + Assert.NotNull(content.Value.Schema); + Assert.NotNull(content.Value.Schema.AllOf); + Assert.Equal("TestDto", content.Value.Schema.AllOf[0].Reference.Id); + Assert.Equal("TypeWithDefaultAttributeOnEnum", content.Value.Schema.AllOf[1].Reference.Id); + } + private static SwaggerGenerator Subject( IEnumerable apiDescriptions, SwaggerGeneratorOptions options = null, diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.GetSwagger_GenerateConsumesSchemas_ForProvidedOpenApiOperationWithSeveralFromForms.verified.txt b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.GetSwagger_GenerateConsumesSchemas_ForProvidedOpenApiOperationWithSeveralFromForms.verified.txt new file mode 100644 index 0000000000..6c22fe28bc --- /dev/null +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.GetSwagger_GenerateConsumesSchemas_ForProvidedOpenApiOperationWithSeveralFromForms.verified.txt @@ -0,0 +1,174 @@ +{ + Info: { + Title: Test API, + Version: V1 + }, + Paths: { + /resource: { + Operations: { + Post: { + OperationId: OperationIdSetInMetadata, + RequestBody: { + UnresolvedReference: false, + Required: false, + Content: { + application/someMediaType: { + Schema: { + ReadOnly: false, + WriteOnly: false, + AllOf: [ + { + ReadOnly: false, + WriteOnly: false, + AdditionalPropertiesAllowed: true, + Nullable: false, + Deprecated: false, + UnresolvedReference: false, + Reference: { + IsFragrament: false, + Type: Schema, + Id: TestDto, + IsExternal: false, + IsLocal: true, + ReferenceV3: #/components/schemas/TestDto, + ReferenceV2: #/definitions/TestDto + } + }, + { + ReadOnly: false, + WriteOnly: false, + AdditionalPropertiesAllowed: true, + Nullable: false, + Deprecated: false, + UnresolvedReference: false, + Reference: { + IsFragrament: false, + Type: Schema, + Id: TypeWithDefaultAttributeOnEnum, + IsExternal: false, + IsLocal: true, + ReferenceV3: #/components/schemas/TypeWithDefaultAttributeOnEnum, + ReferenceV2: #/definitions/TypeWithDefaultAttributeOnEnum + } + } + ], + AdditionalPropertiesAllowed: true, + Nullable: false, + Deprecated: false, + UnresolvedReference: false + } + } + } + }, + Deprecated: false + } + }, + UnresolvedReference: false + } + }, + Components: { + Schemas: { + IntEnum: { + Type: integer, + Format: int32, + ReadOnly: false, + WriteOnly: false, + AdditionalPropertiesAllowed: true, + Enum: [ + { + Value: 2 + }, + { + Value: 4 + }, + { + Value: 8 + } + ], + Nullable: false, + Deprecated: false, + UnresolvedReference: false + }, + TestDto: { + Type: object, + ReadOnly: false, + WriteOnly: false, + Properties: { + Prop1: { + Type: string, + ReadOnly: false, + WriteOnly: false, + AdditionalPropertiesAllowed: true, + Nullable: true, + Deprecated: false, + UnresolvedReference: false + } + }, + AdditionalPropertiesAllowed: false, + Nullable: false, + Deprecated: false, + UnresolvedReference: false + }, + TypeWithDefaultAttributeOnEnum: { + Type: object, + ReadOnly: false, + WriteOnly: false, + Properties: { + EnumArrayWithDefault: { + Type: array, + Default: [ + { + Value: 4 + } + ], + ReadOnly: false, + WriteOnly: false, + Items: { + ReadOnly: false, + WriteOnly: false, + AdditionalPropertiesAllowed: true, + Nullable: false, + Deprecated: false, + UnresolvedReference: false, + Reference: { + IsFragrament: false, + Type: Schema, + Id: IntEnum, + IsExternal: false, + IsLocal: true, + ReferenceV3: #/components/schemas/IntEnum, + ReferenceV2: #/definitions/IntEnum + } + }, + AdditionalPropertiesAllowed: true, + Nullable: true, + Deprecated: false, + UnresolvedReference: false + }, + EnumWithDefault: { + ReadOnly: false, + WriteOnly: false, + AdditionalPropertiesAllowed: true, + Nullable: false, + Deprecated: false, + UnresolvedReference: false, + Reference: { + IsFragrament: false, + Type: Schema, + Id: IntEnum, + IsExternal: false, + IsLocal: true, + ReferenceV3: #/components/schemas/IntEnum, + ReferenceV2: #/definitions/IntEnum + } + } + }, + AdditionalPropertiesAllowed: false, + Nullable: false, + Deprecated: false, + UnresolvedReference: false + } + } + }, + HashCode: 41D7DF10C7C0CE16E982FDA61A24E00E80C545544532DDE161A3189D46D8B3F2FD312173BC4F903FA4F3D695D66A00CEF815217B8B865479D45961D02D3B8609 +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.cs index 85de320a2b..3957678a61 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.cs @@ -1149,6 +1149,63 @@ public Task GetSwagger_Copies_Description_From_GeneratedSchema() return Verifier.Verify(document); } + [Fact] + public Task GetSwagger_GenerateConsumesSchemas_ForProvidedOpenApiOperationWithSeveralFromForms() + { + var methodInfo = typeof(FakeController).GetMethod(nameof(FakeController.ActionWithConsumesAttribute)); + var actionDescriptor = new ActionDescriptor + { + EndpointMetadata = + [ + new OpenApiOperation + { + OperationId = "OperationIdSetInMetadata", + RequestBody = new() + { + Content = new Dictionary() + { + ["application/someMediaType"] = new() + } + } + } + ], + RouteValues = new Dictionary + { + ["controller"] = methodInfo.DeclaringType.Name.Replace("Controller", string.Empty) + } + }; + var subject = Subject( + apiDescriptions: + [ + ApiDescriptionFactory.Create( + actionDescriptor, + methodInfo, + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + parameterDescriptions: + [ + new ApiParameterDescription() + { + Name = "param", + Source = BindingSource.Form, + ModelMetadata = ModelMetadataFactory.CreateForType(typeof(TestDto)) + }, + new ApiParameterDescription() + { + Name = "param2", + Source = BindingSource.Form, + ModelMetadata = ModelMetadataFactory.CreateForType(typeof(TypeWithDefaultAttributeOnEnum)) + } + ]), + ] + ); + + var document = subject.GetSwagger("v1"); + + return Verifier.Verify(document); + } + private static SwaggerGenerator Subject( IEnumerable apiDescriptions, SwaggerGeneratorOptions options = null, diff --git a/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs b/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs new file mode 100644 index 0000000000..58b26ebd1c --- /dev/null +++ b/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Mvc; + +namespace WebApi.EndPoints +{ + /// + /// Class of Extensions to add WithOpenApiEndpoints + /// + public static class OpenApiEndpoints + { + /// + /// Extension to add WithOpenApiEndpoints + /// + public static IEndpointRouteBuilder MapWithOpenApiEndpoints(this IEndpointRouteBuilder app) + { + string[] summaries = [ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + ]; + + var group = app.MapGroup("/WithOpenApi").WithTags("WithOpenApi"); + + group.MapGet("weatherforecast", () => + { + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; + }) + .WithName("GetWeatherForecast") + .WithOpenApi(); + + group.MapPost("/api/people-minimalapi", ([FromForm] Person person, [FromForm] Address address) => + { + TypedResults.NoContent(); + }) + .WithOpenApi() + .DisableAntiforgery(); + + + + //group.MapGet("DateTimeKind", (DateTimeKind d) => d); + + return app; + } + } + record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) + { + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } + record class Person(string FirstName, string LastName); + + record class Address(string Street, string City, string State, string ZipCode); +} diff --git a/test/WebSites/WebApi/EndPoints/SwaggerAnnotationsEndpoints.cs b/test/WebSites/WebApi/EndPoints/SwaggerAnnotationsEndpoints.cs new file mode 100644 index 0000000000..66d317b230 --- /dev/null +++ b/test/WebSites/WebApi/EndPoints/SwaggerAnnotationsEndpoints.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +namespace WebApi.EndPoints +{ + /// + /// Class of Extensions to add AnnotationsEndpoints + /// + public static class SwaggerAnnotationsEndpoints + { + /// + /// Extension to add AnnotationsEndpoints + /// + public static IEndpointRouteBuilder MapAnnotationsEndpoints(this IEndpointRouteBuilder app) + { + var group = app.MapGroup("/annotations").WithTags("Annotations"); + + group.MapPost("/fruit/{id}", CreateFruit); + app.MapGet("/kk/{id}", (int id) => id).WithGroupName("Wathever"); + + return app; + } + + [SwaggerResponse(200, "Description for response", typeof(Fruit))] + [SwaggerOperation("CreateFruit", "Create a fruit")] + private static Fruit CreateFruit([AsParameters] CreateFruitModel createFruitModel) => createFruitModel.Fruit; + } + record struct CreateFruitModel + ([FromRoute, SwaggerParameter(Description = "The id of the fruit that will be created", Required = true)] string Id, + [FromBody, SwaggerRequestBody("Description for Body")] Fruit Fruit); + + [SwaggerSchema("Description for Schema")] + record Fruit(string Name); +} diff --git a/test/WebSites/WebApi/EndPoints/XmlCommentsEndpoints.cs b/test/WebSites/WebApi/EndPoints/XmlCommentsEndpoints.cs new file mode 100644 index 0000000000..57c3fec456 --- /dev/null +++ b/test/WebSites/WebApi/EndPoints/XmlCommentsEndpoints.cs @@ -0,0 +1,56 @@ +namespace WebApi.EndPoints +{ + + /// + /// Class of Extensions to add XmlEndpoints + /// + public static class XmlCommentsEndpoints + { + /// + /// Extension to add AnnotationsEndpoints + /// + public static IEndpointRouteBuilder MapXmlCommentsEndpoints(this IEndpointRouteBuilder app) + { + var group = app.MapGroup("/XmlComments").WithTags("Xml"); + + group.MapGet("/Car/{id}", GetProduct); + + app.MapGet("/AsParameters", ([AsParameters] AsParametersArgument request) => "Hello World!"); + + return app; + } + /// + /// Returns a specific product + /// + /// The product id + /// A Product Id + private static Product GetProduct(int id) => new Product { Id = id, Description = "A product" }; + } + /// + /// Represents a product + /// + public class Product + { + /// + /// Uniquely identifies the product + /// + public int Id { get; set; } + + /// + /// Describes the product + /// + public string? Description { get; set; } + } + internal struct AsParametersArgument + { + /// + /// This is a property with the number one - This is nowhere in SwaggerUI + /// + public string PropertyOne { get; set; } + + /// + /// This is a property with the number two - This is nowhere in SwaggerUI + /// + public string PropertyTwo { get; set; } + } +} diff --git a/test/WebSites/WebApi/Program.cs b/test/WebSites/WebApi/Program.cs index 6621e3cf18..73d53917dc 100644 --- a/test/WebSites/WebApi/Program.cs +++ b/test/WebSites/WebApi/Program.cs @@ -1,20 +1,25 @@ -using Microsoft.AspNetCore.Mvc; -using Swashbuckle.AspNetCore.Annotations; +using System.Reflection; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Http.Json; +using WebApi.EndPoints; var builder = WebApplication.CreateBuilder(args); +builder.Services.Configure( + opt => opt.SerializerOptions.Converters.Add(new JsonStringEnumConverter()) +); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { c.EnableAnnotations(); + c.IncludeXmlComments(Assembly.GetExecutingAssembly()); c.SwaggerDoc("v1", new() { Title = "WebApi", Version = "v1" }); }); - var app = builder.Build(); - app.UseSwagger(); app.UseSwaggerUI(); + app.UseHttpsRedirection(); var summaries = new[] @@ -22,40 +27,17 @@ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; -app.MapGet("/weatherforecast", () => -{ - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; -}) -.WithName("GetWeatherForecast") -.WithOpenApi(); - -app.MapPost("/fruit/{id}", ([AsParameters] CreateFruitModel model) => -{ - return model.Fruit; -}).WithName("CreateFruit") -.WithSummary("Create a fruit"); +app.MapAnnotationsEndpoints() +.MapWithOpenApiEndpoints() +.MapXmlCommentsEndpoints(); app.Run(); -record struct CreateFruitModel - ([FromRoute, SwaggerParameter(Description = "The id of the fruit that will be created", Required = true)] string Id, - [FromBody, SwaggerRequestBody("Description for Body")] Fruit Fruit); -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} -record Fruit(string Name); - namespace WebApi { + /// + /// Main class + /// public partial class Program { // Expose the Program class for use with WebApplicationFactory diff --git a/test/WebSites/WebApi/WebApi.csproj b/test/WebSites/WebApi/WebApi.csproj index 5f0339397b..be737b6c89 100644 --- a/test/WebSites/WebApi/WebApi.csproj +++ b/test/WebSites/WebApi/WebApi.csproj @@ -5,6 +5,7 @@ enable WebApi net8.0 + true From 49fcf098c87bc8b3b7270a80014ba5c3bfb22bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Thu, 27 Jun 2024 11:09:34 +0200 Subject: [PATCH 3/7] Address PR feedback and ensure an exception is thrown(As before), when there are more than 1 Body elements --- .../SwaggerGenerator/SwaggerGenerator.cs | 3 +- ...r_WebApi_swaggerRequestUri=v1.verified.txt | 39 +------------------ ...est.TypesAreRenderedCorrectly.verified.txt | 39 +------------------ .../WebApi/EndPoints/OpenApiEndpoints.cs | 6 +-- .../EndPoints/SwaggerAnnotationsEndpoints.cs | 1 - .../WebApi/EndPoints/XmlCommentsEndpoints.cs | 18 +-------- test/WebSites/WebApi/Program.cs | 3 +- 7 files changed, 9 insertions(+), 100 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index 88e2da6edd..4532e537a6 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -421,8 +421,9 @@ private OpenApiOperation GenerateOpenApiOperationFromMetadata(ApiDescription api foreach (var content in requestContentTypes) { var requestParameters = apiDescription.ParameterDescriptions.Where(desc => desc.IsFromBody() || desc.IsFromForm()); - if (requestParameters is not null) + if (requestParameters.Any()) { + _ = requestParameters.SingleOrDefault(desc => desc.IsFromBody()); if (requestParameters.Count() == 1) { content.Schema = GenerateSchema( diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt index 55573759c0..95ad82bc5d 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt @@ -48,43 +48,6 @@ } } }, - "/AsParameters": { - "get": { - "tags": [ - "WebApi" - ], - "parameters": [ - { - "name": "PropertyOne", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "PropertyTwo", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, "/WithOpenApi/weatherforecast": { "get": { "tags": [ @@ -108,7 +71,7 @@ } } }, - "/WithOpenApi/api/people-minimalapi": { + "/WithOpenApi/multipleForms": { "post": { "tags": [ "WithOpenApi" diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt index 55573759c0..95ad82bc5d 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt @@ -48,43 +48,6 @@ } } }, - "/AsParameters": { - "get": { - "tags": [ - "WebApi" - ], - "parameters": [ - { - "name": "PropertyOne", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "PropertyTwo", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, "/WithOpenApi/weatherforecast": { "get": { "tags": [ @@ -108,7 +71,7 @@ } } }, - "/WithOpenApi/api/people-minimalapi": { + "/WithOpenApi/multipleForms": { "post": { "tags": [ "WithOpenApi" diff --git a/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs b/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs index 58b26ebd1c..d0bf555687 100644 --- a/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs +++ b/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs @@ -33,17 +33,13 @@ public static IEndpointRouteBuilder MapWithOpenApiEndpoints(this IEndpointRouteB .WithName("GetWeatherForecast") .WithOpenApi(); - group.MapPost("/api/people-minimalapi", ([FromForm] Person person, [FromForm] Address address) => + group.MapPost("/multipleForms", ([FromForm] Person person, [FromForm] Address address) => { TypedResults.NoContent(); }) .WithOpenApi() .DisableAntiforgery(); - - - //group.MapGet("DateTimeKind", (DateTimeKind d) => d); - return app; } } diff --git a/test/WebSites/WebApi/EndPoints/SwaggerAnnotationsEndpoints.cs b/test/WebSites/WebApi/EndPoints/SwaggerAnnotationsEndpoints.cs index 66d317b230..85508afce1 100644 --- a/test/WebSites/WebApi/EndPoints/SwaggerAnnotationsEndpoints.cs +++ b/test/WebSites/WebApi/EndPoints/SwaggerAnnotationsEndpoints.cs @@ -15,7 +15,6 @@ public static IEndpointRouteBuilder MapAnnotationsEndpoints(this IEndpointRouteB var group = app.MapGroup("/annotations").WithTags("Annotations"); group.MapPost("/fruit/{id}", CreateFruit); - app.MapGet("/kk/{id}", (int id) => id).WithGroupName("Wathever"); return app; } diff --git a/test/WebSites/WebApi/EndPoints/XmlCommentsEndpoints.cs b/test/WebSites/WebApi/EndPoints/XmlCommentsEndpoints.cs index 57c3fec456..d799b12159 100644 --- a/test/WebSites/WebApi/EndPoints/XmlCommentsEndpoints.cs +++ b/test/WebSites/WebApi/EndPoints/XmlCommentsEndpoints.cs @@ -15,8 +15,6 @@ public static IEndpointRouteBuilder MapXmlCommentsEndpoints(this IEndpointRouteB group.MapGet("/Car/{id}", GetProduct); - app.MapGet("/AsParameters", ([AsParameters] AsParametersArgument request) => "Hello World!"); - return app; } /// @@ -24,12 +22,12 @@ public static IEndpointRouteBuilder MapXmlCommentsEndpoints(this IEndpointRouteB /// /// The product id /// A Product Id - private static Product GetProduct(int id) => new Product { Id = id, Description = "A product" }; + private static Product GetProduct(int id) => new() { Id = id, Description = "A product" }; } /// /// Represents a product /// - public class Product + internal class Product { /// /// Uniquely identifies the product @@ -41,16 +39,4 @@ public class Product /// public string? Description { get; set; } } - internal struct AsParametersArgument - { - /// - /// This is a property with the number one - This is nowhere in SwaggerUI - /// - public string PropertyOne { get; set; } - - /// - /// This is a property with the number two - This is nowhere in SwaggerUI - /// - public string PropertyTwo { get; set; } - } } diff --git a/test/WebSites/WebApi/Program.cs b/test/WebSites/WebApi/Program.cs index 73d53917dc..ee4f51266a 100644 --- a/test/WebSites/WebApi/Program.cs +++ b/test/WebSites/WebApi/Program.cs @@ -15,11 +15,12 @@ c.IncludeXmlComments(Assembly.GetExecutingAssembly()); c.SwaggerDoc("v1", new() { Title = "WebApi", Version = "v1" }); }); + var app = builder.Build(); + app.UseSwagger(); app.UseSwaggerUI(); - app.UseHttpsRedirection(); var summaries = new[] From a911f007b3fc6b9a10248ff2e9767d5d8548ac83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Thu, 27 Jun 2024 11:25:39 +0200 Subject: [PATCH 4/7] Smarter Linq --- .../SwaggerGenerator/SwaggerGenerator.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index 4532e537a6..cd98d40daf 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -421,16 +421,18 @@ private OpenApiOperation GenerateOpenApiOperationFromMetadata(ApiDescription api foreach (var content in requestContentTypes) { var requestParameters = apiDescription.ParameterDescriptions.Where(desc => desc.IsFromBody() || desc.IsFromForm()); - if (requestParameters.Any()) + var countOfParameters = requestParameters.Count(); + if (countOfParameters >= 1) { _ = requestParameters.SingleOrDefault(desc => desc.IsFromBody()); - if (requestParameters.Count() == 1) + if (countOfParameters == 1) { + var requestParameter = requestParameters.First(); content.Schema = GenerateSchema( - requestParameters.First().ModelMetadata.ModelType, + requestParameter.ModelMetadata.ModelType, schemaRepository, - requestParameters.First().PropertyInfo(), - requestParameters.First().ParameterInfo()); + requestParameter.PropertyInfo(), + requestParameter.ParameterInfo()); } else { From 85c7d8aa134a74c4596a67a3b0161b5e36e6da9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Thu, 27 Jun 2024 12:23:42 +0200 Subject: [PATCH 5/7] No need of throw exception, it's impossible to reach that code in case there are more than one FromBody --- .../SwaggerGenerator/SwaggerGenerator.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index cd98d40daf..e422bcb874 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -422,9 +422,8 @@ private OpenApiOperation GenerateOpenApiOperationFromMetadata(ApiDescription api { var requestParameters = apiDescription.ParameterDescriptions.Where(desc => desc.IsFromBody() || desc.IsFromForm()); var countOfParameters = requestParameters.Count(); - if (countOfParameters >= 1) + if (countOfParameters > 0) { - _ = requestParameters.SingleOrDefault(desc => desc.IsFromBody()); if (countOfParameters == 1) { var requestParameter = requestParameters.First(); From be22e1e14bf991a82b1ab5229c5e94eee66a4343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Thu, 27 Jun 2024 15:23:08 +0200 Subject: [PATCH 6/7] Put attribute on SwaggerVerifyTest to avoid run the test in paralel --- .../SwaggerVerifyIntegrationTest.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.cs index d73765adbe..142969bff3 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.cs @@ -8,6 +8,7 @@ namespace Swashbuckle.AspNetCore.IntegrationTests { + [Collection("TestSite")] public class SwaggerVerifyIntegrationTest { [Theory] @@ -48,8 +49,8 @@ public async Task SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi( public async Task SwaggerEndpoint_ReturnsValidSwaggerJson_For_Mvc( string swaggerRequestUri) { - var swaggerResponse = await SwaggerEndpointReturnsValidSwaggerJson(swaggerRequestUri); - await Verifier.Verify(swaggerResponse).UseParameters(GetVersion(swaggerRequestUri)); + var swaggerResponse = await SwaggerEndpointReturnsValidSwaggerJson(swaggerRequestUri); + await Verifier.Verify(swaggerResponse).UseParameters(GetVersion(swaggerRequestUri)); } [Fact] @@ -58,7 +59,7 @@ public async Task TypesAreRenderedCorrectly() using var application = new TestApplication(); using var client = application.CreateDefaultClient(); - var swaggerResponse = await SwaggerResponse(client,"/swagger/v1/swagger.json"); + var swaggerResponse = await SwaggerResponse(client, "/swagger/v1/swagger.json"); await Verifier.Verify(swaggerResponse); } From 914dd1921d65e665b3ca3521d7ed2c7f1af10f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Thu, 27 Jun 2024 16:04:23 +0200 Subject: [PATCH 7/7] Delete blank line Co-authored-by: Martin Costello --- .../SwaggerGenerator/SwaggerGenerator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index e422bcb874..6234e1ad4d 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -446,7 +446,6 @@ private OpenApiOperation GenerateOpenApiOperationFromMetadata(ApiDescription api .ToList() }; } - } } }