diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index fea023d648..0508666338 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -132,17 +132,11 @@ private IDictionary GenerateOperations( private OpenApiOperation GenerateOperation(ApiDescription apiDescription, SchemaRepository schemaRepository) { -#if NET6_0_OR_GREATER - var metadata = apiDescription.ActionDescriptor?.EndpointMetadata; - var existingOperation = metadata?.OfType().SingleOrDefault(); - if (existingOperation != null) - { - return existingOperation; - } -#endif + OpenApiOperation operation = GenerateOpenApiOperationFromMetadata(apiDescription, schemaRepository); + try { - var operation = new OpenApiOperation + operation ??= new OpenApiOperation { Tags = GenerateOperationTags(apiDescription), OperationId = _options.OperationIdSelector(apiDescription), @@ -169,6 +163,66 @@ private OpenApiOperation GenerateOperation(ApiDescription apiDescription, Schema } } + private OpenApiOperation GenerateOpenApiOperationFromMetadata(ApiDescription apiDescription, SchemaRepository schemaRepository) + { +#if NET6_0_OR_GREATER + var metadata = apiDescription.ActionDescriptor?.EndpointMetadata; + var operation = metadata?.OfType().SingleOrDefault(); + + if (operation is null) + { + return null; + } + + // Schemas will be generated via Swashbuckle by default. + foreach (var parameter in operation.Parameters) + { + var apiParameter = apiDescription.ParameterDescriptions.SingleOrDefault(desc => desc.Name == parameter.Name); + if (apiParameter is not null) + { + parameter.Schema = GenerateSchema( + apiParameter.ModelMetadata.ModelType, + schemaRepository, + apiParameter.PropertyInfo(), + apiParameter.ParameterInfo(), + apiParameter.RouteInfo); + } + } + + var requestContentTypes = operation.RequestBody?.Content?.Values; + foreach (var content in requestContentTypes ?? Enumerable.Empty()) + { + var requestParameter = apiDescription.ParameterDescriptions.SingleOrDefault(desc => desc.IsFromBody() || desc.IsFromBody()); + if (requestParameter is not null) + { + content.Schema = GenerateSchema( + requestParameter.ModelMetadata.ModelType, + schemaRepository, + requestParameter.PropertyInfo(), + requestParameter.ParameterInfo()); + } + } + + foreach (var kvp in operation.Responses) + { + var response = kvp.Value; + var responseModel = apiDescription.SupportedResponseTypes.SingleOrDefault(desc => desc.StatusCode.ToString() == kvp.Key); + if (responseModel is not null) + { + var responseContentTypes = response?.Content?.Values; + foreach (var content in responseContentTypes ?? Enumerable.Empty()) + { + content.Schema = GenerateSchema(responseModel.Type, schemaRepository); + } + } + } + + return operation; +#else + return null; +#endif + } + private IList GenerateOperationTags(ApiDescription apiDescription) { return _options.TagsSelector(apiDescription) diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs index 36439a4415..60e01178e5 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs @@ -179,6 +179,173 @@ public void GetSwagger_UseProvidedOpenApiOperation_IfExistsInMetadata() Assert.Equal("ParameterInMetadata", document.Paths["/resource"].Operations[OperationType.Post].Parameters[0].Name); } + [Fact] + public void GetSwagger_GenerateProducesSchemas_ForProvidedOpenApiOperation() + { + var methodInfo = typeof(FakeController).GetMethod(nameof(FakeController.ActionWithProducesAttribute)); + var actionDescriptor = new ActionDescriptor + { + EndpointMetadata = new List() + { + new OpenApiOperation + { + OperationId = "OperationIdSetInMetadata", + Responses = new() + { + ["200"] = new() + { + Content = new Dictionary() + { + ["application/someMediaType"] = new() + } + } + } + } + }, + RouteValues = new Dictionary + { + ["controller"] = methodInfo.DeclaringType.Name.Replace("Controller", string.Empty) + } + }; + var subject = Subject( + apiDescriptions: new[] + { + ApiDescriptionFactory.Create( + actionDescriptor, + methodInfo, + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + supportedResponseTypes: new[] + { + new ApiResponseType() + { + StatusCode = 200, + Type = typeof(TestDto) + } + }), + } + ); + + 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].Responses["200"].Content); + Assert.Equal("application/someMediaType", content.Key); + Assert.Null(content.Value.Schema.Type); + Assert.NotNull(content.Value.Schema.Reference); + Assert.Equal("TestDto", content.Value.Schema.Reference.Id); + } + + [Fact] + public void GetSwagger_GenerateConsumesSchemas_ForProvidedOpenApiOperation() + { + var methodInfo = typeof(FakeController).GetMethod(nameof(FakeController.ActionWithConsumesAttribute)); + var actionDescriptor = new ActionDescriptor + { + EndpointMetadata = new List() + { + 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: new[] + { + ApiDescriptionFactory.Create( + actionDescriptor, + methodInfo, + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + parameterDescriptions: new[] + { + new ApiParameterDescription() + { + Name = "param", + Source = BindingSource.Body, + ModelMetadata = ModelMetadataFactory.CreateForType(typeof(TestDto)) + } + }), + } + ); + + 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.Null(content.Value.Schema.Type); + Assert.NotNull(content.Value.Schema.Reference); + Assert.Equal("TestDto", content.Value.Schema.Reference.Id); + } + + [Fact] + public void GetSwagger_GenerateParametersSchemas_ForProvidedOpenApiOperation() + { + var methodInfo = typeof(FakeController).GetMethod(nameof(FakeController.ActionWithParameter)); + var actionDescriptor = new ActionDescriptor + { + EndpointMetadata = new List() + { + new OpenApiOperation + { + OperationId = "OperationIdSetInMetadata", + Parameters = new List() + { + new OpenApiParameter + { + Name = "ParameterInMetadata" + } + } + } + }, + RouteValues = new Dictionary + { + ["controller"] = methodInfo.DeclaringType.Name.Replace("Controller", string.Empty) + } + }; + var subject = Subject( + apiDescriptions: new[] + { + ApiDescriptionFactory.Create( + actionDescriptor, + methodInfo, + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + parameterDescriptions: new[] + { + new ApiParameterDescription + { + Name = "ParameterInMetadata", + ModelMetadata = ModelMetadataFactory.CreateForType(typeof(string)) + } + }), + } + ); + + var document = subject.GetSwagger("v1"); + + Assert.Equal("OperationIdSetInMetadata", document.Paths["/resource"].Operations[OperationType.Post].OperationId); + Assert.Equal("ParameterInMetadata", document.Paths["/resource"].Operations[OperationType.Post].Parameters[0].Name); + Assert.NotNull(document.Paths["/resource"].Operations[OperationType.Post].Parameters[0].Schema); + Assert.Equal("string", document.Paths["/resource"].Operations[OperationType.Post].Parameters[0].Schema.Type); + } + [Fact] public void GetSwagger_SetsOperationIdToNull_IfActionHasNoEndpointMetadata() {