From f1982bd987b0fb7d97b4125779e893ac759782c3 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Tue, 7 Jun 2016 17:22:47 -0700 Subject: [PATCH] Suppress default status code response type in api descriptions when explicit response types have been provided [Fixes #4823] How to override the default (200) status code with ProducesResponseType --- .../DefaultApiDescriptionProvider.cs | 15 ++- .../ApiExplorerTest.cs | 127 ++++++++++++++++++ ...orerResponseTypeWithAttributeController.cs | 32 ++++- 3 files changed, 166 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs index 2483e89ca9..be65b98928 100644 --- a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs @@ -358,13 +358,6 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer // Build list of all possible return types (and status codes) for an action. var objectTypes = new Dictionary(); - if (type != null && type != typeof(void)) - { - // This return type can be overriden by any response metadata - // attributes later if the user wishes to. - objectTypes[StatusCodes.Status200OK] = type; - } - // Get the content type that the action explicitly set to support. // Walk through all 'filter' attributes in order, and allow each one to see or override // the results of the previous ones. This is similar to the execution path for content-negotiation. @@ -382,6 +375,14 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer } } + // Set the default status only when no status has already been set explicitly + if (objectTypes.Count == 0 + && type != null + && type != typeof(void)) + { + objectTypes[StatusCodes.Status200OK] = type; + } + if (contentTypes.Count == 0) { contentTypes.Add((string)null); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs index dbc224ae35..ece3d7e084 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs @@ -523,6 +523,133 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("application/json", responseFormat.MediaType); } + [Fact] + public async Task ExplicitResponseTypeDecoration_SuppressesDefaultStatus() + { + // Arrange + var type1 = typeof(ApiExplorerWebSite.Product).FullName; + var type2 = typeof(ModelStateDictionary).FullName; + var expectedMediaTypes = new[] { "application/json", "text/json", "application/xml", "text/xml" }; + + // Act + var response = await Client.GetAsync( + "http://localhost/ApiExplorerResponseTypeWithAttribute/CreateProductWithDefaultResponseContentTypes"); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal(2, description.SupportedResponseTypes.Count); + var responseType = description.SupportedResponseTypes[0]; + Assert.Equal(type1, responseType.ResponseType); + Assert.Equal(201, responseType.StatusCode); + Assert.Equal( + expectedMediaTypes, + responseType.ResponseFormats.Select(responseFormat => responseFormat.MediaType).ToArray()); + responseType = description.SupportedResponseTypes[1]; + Assert.Equal(type2, responseType.ResponseType); + Assert.Equal(400, responseType.StatusCode); + Assert.Equal( + expectedMediaTypes, + responseType.ResponseFormats.Select(responseFormat => responseFormat.MediaType).ToArray()); + } + + [Fact] + public async Task ExplicitResponseTypeDecoration_SuppressesDefaultStatus_AlsoHonorsProducesContentTypes() + { + // Arrange + var type1 = typeof(ApiExplorerWebSite.Product).FullName; + var type2 = typeof(ModelStateDictionary).FullName; + var expectedMediaTypes = new[] { "text/xml" }; + + // Act + var response = await Client.GetAsync( + "http://localhost/ApiExplorerResponseTypeWithAttribute/CreateProductWithLimitedResponseContentTypes"); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal(2, description.SupportedResponseTypes.Count); + var responseType = description.SupportedResponseTypes[0]; + Assert.Equal(type1, responseType.ResponseType); + Assert.Equal(201, responseType.StatusCode); + Assert.Equal( + expectedMediaTypes, + responseType.ResponseFormats.Select(responseFormat => responseFormat.MediaType).ToArray()); + responseType = description.SupportedResponseTypes[1]; + Assert.Equal(type2, responseType.ResponseType); + Assert.Equal(400, responseType.StatusCode); + Assert.Equal( + expectedMediaTypes, + responseType.ResponseFormats.Select(responseFormat => responseFormat.MediaType).ToArray()); + } + + [Fact] + public async Task ExplicitResponseTypeDecoration_WithExplicitDefaultStatus() + { + // Arrange + var type1 = typeof(ApiExplorerWebSite.Product).FullName; + var type2 = typeof(ModelStateDictionary).FullName; + var expectedMediaTypes = new[] { "application/json", "text/json", "application/xml", "text/xml" }; + + // Act + var response = await Client.GetAsync( + "http://localhost/ApiExplorerResponseTypeWithAttribute/UpdateProductWithDefaultResponseContentTypes"); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal(2, description.SupportedResponseTypes.Count); + var responseType = description.SupportedResponseTypes[0]; + Assert.Equal(type1, responseType.ResponseType); + Assert.Equal(200, responseType.StatusCode); + Assert.Equal( + expectedMediaTypes, + responseType.ResponseFormats.Select(responseFormat => responseFormat.MediaType).ToArray()); + responseType = description.SupportedResponseTypes[1]; + Assert.Equal(type2, responseType.ResponseType); + Assert.Equal(400, responseType.StatusCode); + Assert.Equal( + expectedMediaTypes, + responseType.ResponseFormats.Select(responseFormat => responseFormat.MediaType).ToArray()); + } + + [Fact] + public async Task ExplicitResponseTypeDecoration_WithExplicitDefaultStatus_SpecifiedViaProducesAttribute() + { + // Arrange + var type1 = typeof(ApiExplorerWebSite.Product).FullName; + var type2 = typeof(ModelStateDictionary).FullName; + var expectedMediaTypes = new[] { "text/xml" }; + + // Act + var response = await Client.GetAsync( + "http://localhost/ApiExplorerResponseTypeWithAttribute/UpdateProductWithLimitedResponseContentTypes"); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal(2, description.SupportedResponseTypes.Count); + var responseType = description.SupportedResponseTypes[0]; + Assert.Equal(type1, responseType.ResponseType); + Assert.Equal(200, responseType.StatusCode); + Assert.Equal( + expectedMediaTypes, + responseType.ResponseFormats.Select(responseFormat => responseFormat.MediaType).ToArray()); + responseType = description.SupportedResponseTypes[1]; + Assert.Equal(type2, responseType.ResponseType); + Assert.Equal(400, responseType.StatusCode); + Assert.Equal( + expectedMediaTypes, + responseType.ResponseFormats.Select(responseFormat => responseFormat.MediaType).ToArray()); + } [Fact] public async Task ApiExplorer_ResponseType_InheritingFromController() { diff --git a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeWithAttributeController.cs b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeWithAttributeController.cs index f509278696..da08ad855e 100644 --- a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeWithAttributeController.cs +++ b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeWithAttributeController.cs @@ -3,10 +3,11 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace ApiExplorerWebSite { - [Route("ApiExplorerResponseTypeWithAttribute/[Action]")] + [Route("[controller]/[Action]")] public class ApiExplorerResponseTypeWithAttributeController : Controller { [HttpGet] @@ -42,5 +43,34 @@ namespace ApiExplorerWebSite { return null; } + + [ProducesResponseType(typeof(Product), 201)] + [ProducesResponseType(typeof(ModelStateDictionary), 400)] + public Product CreateProductWithDefaultResponseContentTypes(Product product) + { + return null; + } + + [Produces("text/xml")] // Has status code as 200 but is not applied as it does not set 'Type' + [ProducesResponseType(typeof(Product), 201)] + [ProducesResponseType(typeof(ModelStateDictionary), 400)] + public Product CreateProductWithLimitedResponseContentTypes(Product product) + { + return null; + } + + [ProducesResponseType(typeof(Product), 200)] + [ProducesResponseType(typeof(ModelStateDictionary), 400)] + public Product UpdateProductWithDefaultResponseContentTypes(Product product) + { + return null; + } + + [Produces("text/xml", Type = typeof(Product))] // Has status code as 200 + [ProducesResponseType(typeof(ModelStateDictionary), 400)] + public Product UpdateProductWithLimitedResponseContentTypes(Product product) + { + return null; + } } } \ No newline at end of file