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/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index c80f8657cb..6234e1ad4d 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -420,14 +420,32 @@ 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()); + var countOfParameters = requestParameters.Count(); + if (countOfParameters > 0) { - content.Schema = GenerateSchema( - requestParameter.ModelMetadata.ModelType, - schemaRepository, - requestParameter.PropertyInfo(), - requestParameter.ParameterInfo()); + if (countOfParameters == 1) + { + var requestParameter = requestParameters.First(); + content.Schema = GenerateSchema( + requestParameter.ModelMetadata.ModelType, + schemaRepository, + requestParameter.PropertyInfo(), + requestParameter.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..95ad82bc5d --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt @@ -0,0 +1,240 @@ +{ + "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" + } + } + } + } + } + } + }, + "/WithOpenApi/weatherforecast": { + "get": { + "tags": [ + "WithOpenApi" + ], + "operationId": "GetWeatherForecast", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WeatherForecast" + } + } + } + } + } + } + } + }, + "/WithOpenApi/multipleForms": { + "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..95ad82bc5d --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt @@ -0,0 +1,240 @@ +{ + "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" + } + } + } + } + } + } + }, + "/WithOpenApi/weatherforecast": { + "get": { + "tags": [ + "WithOpenApi" + ], + "operationId": "GetWeatherForecast", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WeatherForecast" + } + } + } + } + } + } + } + }, + "/WithOpenApi/multipleForms": { + "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..142969bff3 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.cs @@ -0,0 +1,83 @@ +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 +{ + [Collection("TestSite")] + 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..d0bf555687 --- /dev/null +++ b/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs @@ -0,0 +1,53 @@ +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("/multipleForms", ([FromForm] Person person, [FromForm] Address address) => + { + TypedResults.NoContent(); + }) + .WithOpenApi() + .DisableAntiforgery(); + + 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..85508afce1 --- /dev/null +++ b/test/WebSites/WebApi/EndPoints/SwaggerAnnotationsEndpoints.cs @@ -0,0 +1,32 @@ +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); + + 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..d799b12159 --- /dev/null +++ b/test/WebSites/WebApi/EndPoints/XmlCommentsEndpoints.cs @@ -0,0 +1,42 @@ +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); + + return app; + } + /// + /// Returns a specific product + /// + /// The product id + /// A Product Id + private static Product GetProduct(int id) => new() { Id = id, Description = "A product" }; + } + /// + /// Represents a product + /// + internal class Product + { + /// + /// Uniquely identifies the product + /// + public int Id { get; set; } + + /// + /// Describes the product + /// + public string? Description { get; set; } + } +} diff --git a/test/WebSites/WebApi/Program.cs b/test/WebSites/WebApi/Program.cs index 834c8a8c49..ee4f51266a 100644 --- a/test/WebSites/WebApi/Program.cs +++ b/test/WebSites/WebApi/Program.cs @@ -1,12 +1,18 @@ -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" }); }); @@ -22,39 +28,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"); +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] 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