diff --git a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiDescription.cs b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiDescription.cs
index dddafbd826..07265342d6 100644
--- a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiDescription.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiDescription.cs
@@ -43,23 +43,6 @@ public class ApiDescription
///
public string RelativePath { get; set; }
- ///
- /// Gets or sets for the or null.
- ///
- ///
- /// Will be null if is null.
- ///
- public ModelMetadata ResponseModelMetadata { get; set; }
-
- ///
- /// Gets or sets the CLR data type of the response or null.
- ///
- ///
- /// Will be null if the action returns no response, or if the response type is unclear. Use
- /// ProducesAttribute on an action method to specify a response type.
- ///
- public Type ResponseType { get; set; }
-
///
/// Gets the list of possible formats for a response.
///
diff --git a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseFormat.cs b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseFormat.cs
index a02a5595fd..cbc77adbed 100644
--- a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseFormat.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseFormat.cs
@@ -1,7 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using Microsoft.AspNetCore.Mvc.Formatters;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
@@ -19,5 +21,27 @@ public class ApiResponseFormat
/// The media type of the response.
///
public string MediaType { get; set; }
+
+ ///
+ /// Gets or sets for the or null.
+ ///
+ ///
+ /// Will be null if is null or void.
+ ///
+ public ModelMetadata ResponseModelMetadata { get; set; }
+
+ ///
+ /// Gets or sets the CLR data type of the response or null.
+ ///
+ ///
+ /// Will be null if the action returns no response, or if the response type is unclear. Use
+ /// ProducesAttribute on an action method to specify a response type.
+ ///
+ public Type ResponseType { get; set; }
+
+ ///
+ /// Http response status code
+ ///
+ public int StatusCode { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs
index dcab0a3d8c..a541880547 100644
--- a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs
@@ -114,35 +114,22 @@ private ApiDescription CreateApiDescription(
// Now 'simulate' an action execution. This attempts to figure out to the best of our knowledge
// what the logical data type is using filters.
- var runtimeReturnType = GetRuntimeReturnType(declaredReturnType, responseMetadataAttributes);
+ var runtimeReturnType = GetRuntimeReturnType(declaredReturnType);
// We might not be able to figure out a good runtime return type. If that's the case we don't
// provide any information about outputs. The workaround is to attribute the action.
- if (runtimeReturnType == typeof(void))
+ var responseFormats = GetResponseFormats(action, responseMetadataAttributes, runtimeReturnType);
+ foreach (var responseFormat in responseFormats)
{
- // As a special case, if the return type is void - we want to surface that information
- // specifically, but nothing else. This can be overridden with a filter/attribute.
- apiDescription.ResponseType = runtimeReturnType;
- }
- else if (runtimeReturnType != null)
- {
- apiDescription.ResponseType = runtimeReturnType;
-
- apiDescription.ResponseModelMetadata = _modelMetadataProvider.GetMetadataForType(runtimeReturnType);
-
- var formats = GetResponseFormats(action, responseMetadataAttributes, runtimeReturnType);
- foreach (var format in formats)
- {
- apiDescription.SupportedResponseFormats.Add(format);
- }
+ apiDescription.SupportedResponseFormats.Add(responseFormat);
}
// It would be possible here to configure an action with multiple body parameters, in which case you
// could end up with duplicate data.
foreach (var parameter in apiDescription.ParameterDescriptions.Where(p => p.Source == BindingSource.Body))
{
- var formats = GetRequestFormats(action, requestMetadataAttributes, parameter.Type);
- foreach (var format in formats)
+ var requestFormats = GetRequestFormats(action, requestMetadataAttributes, parameter.Type);
+ foreach (var format in requestFormats)
{
apiDescription.SupportedRequestFormats.Add(format);
}
@@ -371,6 +358,23 @@ private IReadOnlyList GetResponseFormats(
{
var results = new List();
+ // Build list of all possible return types (and status codes) for an action.
+ var responseTypes = new Dictionary();
+
+ // If the action returns a type, then automatically consider it as a possible api response format with
+ // a status code 200 OK.
+ // NOTE: In some cases this might sound incorrect, for example in the case where the action is returning
+ // 'void' or 'Task'. We want to keep it this way to enable users to override the type via the response
+ // metadata attributes. So an exmple is that action returns void but there is a Produces attribute which
+ // sets the response type to be 'Produces'
+ if (type != null)
+ {
+ // This return type can be overriden by any response metadata
+ // attributes later if the user wishes to.
+ responseTypes[200] = 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.
var contentTypes = new MediaTypeCollection();
@@ -379,6 +383,11 @@ private IReadOnlyList GetResponseFormats(
foreach (var metadataAttribute in responseMetadataAttributes)
{
metadataAttribute.SetContentTypes(contentTypes);
+
+ if (metadataAttribute.Type != null)
+ {
+ responseTypes[metadataAttribute.StatusCode] = metadataAttribute.Type;
+ }
}
}
@@ -387,25 +396,46 @@ private IReadOnlyList GetResponseFormats(
contentTypes.Add((string)null);
}
- foreach (var contentType in contentTypes)
+ var responseFormatMetadataProviders = _outputFormatters.OfType();
+
+ foreach (var responseType in responseTypes)
{
- foreach (var formatter in _outputFormatters)
+ if (responseType.Value == typeof(void))
{
- var responseFormatMetadataProvider = formatter as IApiResponseFormatMetadataProvider;
- if (responseFormatMetadataProvider != null)
+ results.Add(new ApiResponseFormat()
{
- var supportedTypes = responseFormatMetadataProvider.GetSupportedContentTypes(contentType, type);
+ StatusCode = responseType.Key,
+ ResponseType = responseType.Value
+ });
- if (supportedTypes != null)
+ continue;
+ }
+
+ var responseTypeModelMetadata = _modelMetadataProvider.GetMetadataForType(responseType.Value);
+
+ foreach (var contentType in contentTypes)
+ {
+ foreach (var responseFormatMetadataProvider in responseFormatMetadataProviders)
+ {
+ var formatterSupportedContentTypes = responseFormatMetadataProvider.GetSupportedContentTypes(
+ contentType,
+ responseType.Value);
+
+ if (formatterSupportedContentTypes == null)
{
- foreach (var supportedType in supportedTypes)
+ continue;
+ }
+
+ foreach (var formatterSupportedContentType in formatterSupportedContentTypes)
+ {
+ results.Add(new ApiResponseFormat()
{
- results.Add(new ApiResponseFormat()
- {
- Formatter = formatter,
- MediaType = supportedType,
- });
- }
+ Formatter = (IOutputFormatter)responseFormatMetadataProvider,
+ MediaType = formatterSupportedContentType,
+ ResponseType = responseType.Value,
+ StatusCode = responseType.Key,
+ ResponseModelMetadata = responseTypeModelMetadata
+ });
}
}
}
@@ -445,28 +475,8 @@ private static Type GetTaskInnerTypeOrNull(Type type)
return genericType?.GenericTypeArguments[0];
}
- private Type GetRuntimeReturnType(Type declaredReturnType, IApiResponseMetadataProvider[] metadataAttributes)
+ private Type GetRuntimeReturnType(Type declaredReturnType)
{
- // Walk through all of the filter attributes and allow them to set the type. This will execute them
- // in filter-order allowing the desired behavior for overriding.
- if (metadataAttributes != null)
- {
- Type typeSetByAttribute = null;
- foreach (var metadataAttribute in metadataAttributes)
- {
- if (metadataAttribute.Type != null)
- {
- typeSetByAttribute = metadataAttribute.Type;
- }
- }
-
- // If one of the filters set a type, then trust it.
- if (typeSetByAttribute != null)
- {
- return typeSetByAttribute;
- }
- }
-
// If we get here, then a filter didn't give us an answer, so we need to figure out if we
// want to use the declared return type.
//
@@ -731,5 +741,18 @@ public int GetHashCode(PropertyKey obj)
}
}
}
+
+ private class ResponseType
+ {
+ public ResponseType(Type type, int statusCode)
+ {
+ Type = type;
+ StatusCode = statusCode;
+ }
+
+ public Type Type { get; }
+
+ public int StatusCode { get; }
+ }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApiExplorer/IApiResponseMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApiExplorer/IApiResponseMetadataProvider.cs
index c7a073e30c..08bcfce58c 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ApiExplorer/IApiResponseMetadataProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ApiExplorer/IApiResponseMetadataProvider.cs
@@ -7,7 +7,8 @@
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
///
- /// Provides a return type and a set of possible content types returned by a successful execution of the action.
+ /// Provides a return type, status code and a set of possible content types returned by a
+ /// successful execution of the action.
///
public interface IApiResponseMetadataProvider
{
@@ -16,6 +17,11 @@ public interface IApiResponseMetadataProvider
///
Type Type { get; }
+ ///
+ /// Http status code of the response.
+ ///
+ int StatusCode { get; }
+
///
/// Configures a collection of allowed content types which can be produced by the action.
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs
index efc2100597..4a2e90ce90 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs
@@ -4,12 +4,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Formatters.Internal;
-using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc
@@ -64,6 +64,14 @@ public ProducesAttribute(string contentType, params string[] additionalContentTy
public MediaTypeCollection ContentTypes { get; set; }
+ public int StatusCode
+ {
+ get
+ {
+ return (int)HttpStatusCode.OK;
+ }
+ }
+
public override void OnResultExecuting(ResultExecutingContext context)
{
if (context == null)
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ProducesResponseTypeAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/ProducesResponseTypeAttribute.cs
new file mode 100644
index 0000000000..168d56dc6b
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ProducesResponseTypeAttribute.cs
@@ -0,0 +1,49 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc.Formatters;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ ///
+ /// Specifies the the type of the value and status code returned by the action
+ /// which can be used to select a formatter while executing .
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
+ public class ProducesResponseTypeAttribute : Attribute, IApiResponseMetadataProvider, IFilterMetadata
+ {
+ ///
+ /// Initializes an instance of .
+ ///
+ /// The of object that is going to be written in the response.
+ /// Http response status code
+ public ProducesResponseTypeAttribute(Type type, int statusCode)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException(nameof(type));
+ }
+
+ Type = type;
+ StatusCode = statusCode;
+ }
+
+ ///
+ /// The type of the value returned by an action.
+ ///
+ public Type Type { get; set; }
+
+ ///
+ /// Http status code response.
+ ///
+ public int StatusCode { get; set; }
+
+ public void SetContentTypes(MediaTypeCollection contentTypes)
+ {
+ // Users are supposed to use the 'Produces' attribute to set the content types that an aciton can support.
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs
index eeaeabe56d..54a1d237f7 100644
--- a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs
@@ -20,7 +20,6 @@
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.Extensions.Options;
-using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
@@ -217,7 +216,7 @@ public void GetApiDescription_PopulatesParametersThatAppearOnRouteTemplate_AndHa
}
// Only a parameter which comes from a route or model binding or unknown should
- // include route info.
+ // include route info.
[Theory]
[InlineData("api/products/{id}", nameof(FromBody), "Body")]
[InlineData("api/products/{id}", nameof(FromHeader), "Header")]
@@ -373,8 +372,12 @@ public void GetApiDescription_PopulatesResponseType_WithProduct()
// Assert
var description = Assert.Single(descriptions);
- Assert.Equal(typeof(Product), description.ResponseType);
- Assert.NotNull(description.ResponseModelMetadata);
+ Assert.Equal(4, description.SupportedResponseFormats.Count);
+ foreach (var responseFormat in description.SupportedResponseFormats)
+ {
+ Assert.Equal(typeof(Product), responseFormat.ResponseType);
+ Assert.NotNull(responseFormat.ResponseModelMetadata);
+ }
}
[Fact]
@@ -388,8 +391,13 @@ public void GetApiDescription_PopulatesResponseType_WithTaskOfProduct()
// Assert
var description = Assert.Single(descriptions);
- Assert.Equal(typeof(Product), description.ResponseType);
- Assert.NotNull(description.ResponseModelMetadata);
+ Assert.Equal(4, description.SupportedResponseFormats.Count);
+
+ foreach (var responseFormat in description.SupportedResponseFormats)
+ {
+ Assert.Equal(typeof(Product), responseFormat.ResponseType);
+ Assert.NotNull(responseFormat.ResponseModelMetadata);
+ }
}
[Theory]
@@ -409,11 +417,185 @@ public void GetApiDescription_DoesNotPopulatesResponseInformation_WhenUnknown(st
// Assert
var description = Assert.Single(descriptions);
- Assert.Null(description.ResponseType);
- Assert.Null(description.ResponseModelMetadata);
Assert.Empty(description.SupportedResponseFormats);
}
+ public static TheoryData>
+ ReturnsActionResultWithProducesAndProducesContentTypeData
+ {
+ get
+ {
+ var filterDescriptors = new List()
+ {
+ new FilterDescriptor(
+ new ProducesAttribute("text/json", "application/json") { Type = typeof(Customer) },
+ FilterScope.Action),
+ new FilterDescriptor(
+ new ProducesResponseTypeAttribute(typeof(BadData), 400),
+ FilterScope.Action),
+ new FilterDescriptor(
+ new ProducesResponseTypeAttribute(typeof(ErrorDetails), 500),
+ FilterScope.Action)
+ };
+
+ return new TheoryData>
+ {
+ {
+ typeof(DefaultApiDescriptionProviderTest),
+ nameof(DefaultApiDescriptionProviderTest.ReturnsTaskOfActionResult),
+ filterDescriptors
+ },
+ {
+ typeof(DefaultApiDescriptionProviderTest),
+ nameof(DefaultApiDescriptionProviderTest.ReturnsActionResult),
+ filterDescriptors
+ },
+ {
+ typeof(DerivedProducesController),
+ nameof(DerivedProducesController.ReturnsActionResult),
+ filterDescriptors
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(ReturnsActionResultWithProducesAndProducesContentTypeData))]
+ public void GetApiDescription_ReturnsActionResultWithProduces_And_ProducesContentType(
+ Type controllerType,
+ string methodName,
+ List filterDescriptors)
+ {
+ // Arrange
+ var action = CreateActionDescriptor(methodName, controllerType);
+ action.FilterDescriptors = filterDescriptors;
+
+ // Act
+ var descriptions = GetApiDescriptions(action);
+
+ // Assert
+ var description = Assert.Single(descriptions);
+ Assert.Equal(6, description.SupportedResponseFormats.Count);
+
+ var formats = description.SupportedResponseFormats.Where(frmt => frmt.ResponseType == typeof(Customer));
+ Assert.Equal(2, formats.Count());
+ var format = Assert.Single(formats.Where(frmt => frmt.MediaType == "text/json"));
+ Assert.Equal(200, format.StatusCode);
+ Assert.NotNull(format.ResponseModelMetadata);
+ format = Assert.Single(formats.Where(frmt => frmt.MediaType == "application/json"));
+ Assert.Equal(200, format.StatusCode);
+ Assert.NotNull(format.ResponseModelMetadata);
+
+ formats = description.SupportedResponseFormats.Where(frmt => frmt.ResponseType == typeof(BadData));
+ Assert.Equal(2, formats.Count());
+ format = Assert.Single(formats.Where(frmt => frmt.MediaType == "text/json"));
+ Assert.Equal(400, format.StatusCode);
+ Assert.NotNull(format.ResponseModelMetadata);
+ format = Assert.Single(formats.Where(frmt => frmt.MediaType == "application/json"));
+ Assert.Equal(400, format.StatusCode);
+ Assert.NotNull(format.ResponseModelMetadata);
+
+ formats = description.SupportedResponseFormats.Where(frmt => frmt.ResponseType == typeof(ErrorDetails));
+ Assert.Equal(2, formats.Count());
+ format = Assert.Single(formats.Where(frmt => frmt.MediaType == "text/json"));
+ Assert.Equal(500, format.StatusCode);
+ Assert.NotNull(format.ResponseModelMetadata);
+ format = Assert.Single(formats.Where(frmt => frmt.MediaType == "application/json"));
+ Assert.Equal(500, format.StatusCode);
+ Assert.NotNull(format.ResponseModelMetadata);
+ }
+
+ public static TheoryData> ReturnsVoidOrTaskWithProducesContentTypeData
+ {
+ get
+ {
+ var filterDescriptors = new List()
+ {
+ // Since action is returning Void or Task, it does not make sense to provide a value for the
+ // 'Type' property to ProducesAttribute. But the same action could return other types of data
+ // based on runtime conditions.
+ new FilterDescriptor(
+ new ProducesAttribute("text/json", "application/json"),
+ FilterScope.Action),
+ new FilterDescriptor(
+ new ProducesResponseTypeAttribute(typeof(BadData), 400),
+ FilterScope.Action),
+ new FilterDescriptor(
+ new ProducesResponseTypeAttribute(typeof(ErrorDetails), 500),
+ FilterScope.Action)
+ };
+
+ return new TheoryData>
+ {
+ {
+ typeof(DefaultApiDescriptionProviderTest),
+ nameof(DefaultApiDescriptionProviderTest.ReturnsVoid),
+ filterDescriptors
+ },
+ {
+ typeof(DefaultApiDescriptionProviderTest),
+ nameof(DefaultApiDescriptionProviderTest.ReturnsTask),
+ filterDescriptors
+ },
+ {
+ typeof(DerivedProducesController),
+ nameof(DerivedProducesController.ReturnsVoid),
+ filterDescriptors
+ },
+ {
+ typeof(DerivedProducesController),
+ nameof(DerivedProducesController.ReturnsTask),
+ filterDescriptors
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(ReturnsVoidOrTaskWithProducesContentTypeData))]
+ public void GetApiDescription_ReturnsVoidWithProducesContentType(
+ Type controllerType,
+ string methodName,
+ List filterDescriptors)
+ {
+ // Arrange
+ var action = CreateActionDescriptor(methodName, controllerType);
+ action.FilterDescriptors = filterDescriptors;
+
+ // Act
+ var descriptions = GetApiDescriptions(action);
+
+ // Assert
+ var description = Assert.Single(descriptions);
+ Assert.Equal(5, description.SupportedResponseFormats.Count);
+
+ var formats = description.SupportedResponseFormats.Where(frmt => frmt.ResponseType == typeof(void));
+ var format = Assert.Single(formats);
+ Assert.Equal(200, format.StatusCode);
+ Assert.Null(format.ResponseModelMetadata);
+ Assert.Null(format.MediaType);
+ Assert.Null(format.Formatter);
+ Assert.Equal(typeof(void), format.ResponseType);
+
+ formats = description.SupportedResponseFormats.Where(frmt => frmt.ResponseType == typeof(BadData));
+ Assert.Equal(2, formats.Count());
+ format = Assert.Single(formats.Where(frmt => frmt.MediaType == "text/json"));
+ Assert.Equal(400, format.StatusCode);
+ Assert.NotNull(format.ResponseModelMetadata);
+ format = Assert.Single(formats.Where(frmt => frmt.MediaType == "application/json"));
+ Assert.Equal(400, format.StatusCode);
+ Assert.NotNull(format.ResponseModelMetadata);
+
+ formats = description.SupportedResponseFormats.Where(frmt => frmt.ResponseType == typeof(ErrorDetails));
+ Assert.Equal(2, formats.Count());
+ format = Assert.Single(formats.Where(frmt => frmt.MediaType == "text/json"));
+ Assert.Equal(500, format.StatusCode);
+ Assert.NotNull(format.ResponseModelMetadata);
+ format = Assert.Single(formats.Where(frmt => frmt.MediaType == "application/json"));
+ Assert.Equal(500, format.StatusCode);
+ Assert.NotNull(format.ResponseModelMetadata);
+ }
+
[Theory]
[InlineData(nameof(ReturnsVoid))]
[InlineData(nameof(ReturnsTask))]
@@ -427,9 +609,9 @@ public void GetApiDescription_DoesNotPopulatesResponseInformation_WhenVoid(strin
// Assert
var description = Assert.Single(descriptions);
- Assert.Equal(typeof(void), description.ResponseType);
- Assert.Null(description.ResponseModelMetadata);
- Assert.Empty(description.SupportedResponseFormats);
+ var responseFormat = Assert.Single(description.SupportedResponseFormats);
+ Assert.Equal(typeof(void), responseFormat.ResponseType);
+ Assert.Null(responseFormat.ResponseModelMetadata);
}
[Theory]
@@ -458,8 +640,14 @@ public void GetApiDescription_PopulatesResponseInformation_WhenSetByFilter(strin
// Assert
var description = Assert.Single(descriptions);
- Assert.Equal(typeof(Order), description.ResponseType);
- Assert.NotNull(description.ResponseModelMetadata);
+ Assert.Equal(2, description.SupportedResponseFormats.Count);
+ foreach (var responseFormat in description.SupportedResponseFormats)
+ {
+ Assert.Equal(typeof(Order), responseFormat.ResponseType);
+ Assert.NotNull(responseFormat.ResponseModelMetadata);
+ Assert.Equal(200, responseFormat.StatusCode);
+ Assert.StartsWith("text/", responseFormat.MediaType);
+ }
}
[Fact]
@@ -527,13 +715,11 @@ public void GetApiDescription_IncludesResponseFormats_FilteredByType()
// Assert
var description = Assert.Single(descriptions);
- Assert.Equal(1, description.SupportedResponseFormats.Count);
- Assert.Equal(typeof(Order), description.ResponseType);
- Assert.NotNull(description.ResponseModelMetadata);
-
- var formats = description.SupportedResponseFormats;
- Assert.Single(formats, f => f.MediaType.ToString() == "text/json");
- Assert.Same(formatters[0], formats[0].Formatter);
+ var responseFormat = Assert.Single(description.SupportedResponseFormats);
+ Assert.Equal(typeof(Order), responseFormat.ResponseType);
+ Assert.NotNull(responseFormat.ResponseModelMetadata);
+ Assert.Equal("text/json", responseFormat.MediaType);
+ Assert.Same(formatters[0], responseFormat.Formatter);
}
[Fact]
@@ -1151,7 +1337,7 @@ private ControllerActionDescriptor CreateActionDescriptor(string methodName = nu
{
action.MethodInfo = controllerType.GetMethod(
methodName ?? "ReturnsObject",
- BindingFlags.Instance | BindingFlags.Public);
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
action.ControllerTypeInfo = controllerType.GetTypeInfo();
action.BoundProperties = new List();
@@ -1356,6 +1542,39 @@ public void FromQueryName([FromQuery] string name)
}
}
+ public class Customer
+ {
+ }
+
+ public class BadData
+ {
+ }
+
+ public class ErrorDetails
+ {
+ }
+
+ public class BaseProducesController : Controller
+ {
+ public IActionResult ReturnsActionResult()
+ {
+ return null;
+ }
+
+ public Task ReturnsTask()
+ {
+ return null;
+ }
+
+ public void ReturnsVoid()
+ {
+ }
+ }
+
+ public class DerivedProducesController : BaseProducesController
+ {
+ }
+
private class Product
{
public int ProductId { get; set; }
@@ -1519,10 +1738,13 @@ private class ContentTypeAttribute :
public ContentTypeAttribute(string mediaType)
{
ContentTypes.Add(mediaType);
+ StatusCode = 200;
}
public MediaTypeCollection ContentTypes { get; } = new MediaTypeCollection();
+ public int StatusCode { get; set; }
+
public Type Type { get; set; }
public void SetContentTypes(MediaTypeCollection contentTypes)
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs
index e121481499..92c6e0b6c3 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System.Linq;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
@@ -406,7 +407,11 @@ public async Task ApiExplorer_ResponseType_VoidWithoutAttribute(string action)
// Assert
var description = Assert.Single(result);
- Assert.Equal(typeof(void).FullName, description.ResponseType);
+ var responseFormat = Assert.Single(description.SupportedResponseFormats);
+ Assert.Equal(typeof(void).FullName, responseFormat.ResponseType);
+ Assert.Null(responseFormat.FormatterType);
+ Assert.Null(responseFormat.MediaType);
+ //Assert.Null(responseFormat.StatusCode);
}
[Theory]
@@ -427,7 +432,7 @@ public async Task ApiExplorer_ResponseType_UnknownWithoutAttribute(string action
// Assert
var description = Assert.Single(result);
- Assert.Null(description.ResponseType);
+ Assert.Empty(description.SupportedResponseFormats);
}
[Theory]
@@ -446,14 +451,69 @@ public async Task ApiExplorer_ResponseType_KnownWithoutAttribute(string action,
// Assert
var description = Assert.Single(result);
- Assert.Equal(type, description.ResponseType);
+ Assert.Equal(4, description.SupportedResponseFormats.Count);
+ Assert.Equal(4, description.SupportedResponseFormats
+ .Where(respData => respData.ResponseType == type && respData.StatusCode == 200)
+ .Count());
+ Assert.Single(description.SupportedResponseFormats, frmt => frmt.MediaType == "text/json");
+ Assert.Single(description.SupportedResponseFormats, frmt => frmt.MediaType == "application/json");
+ Assert.Single(description.SupportedResponseFormats, frmt => frmt.MediaType == "text/xml");
+ Assert.Single(description.SupportedResponseFormats, frmt => frmt.MediaType == "application/xml");
+ }
+
+ [Fact]
+ public async Task ApiExplorer_ResponseType_KnownWithoutAttribute_ReturnVoid()
+ {
+ // Arrange
+ var type = "ApiExplorerWebSite.Customer";
+
+ // Act
+ var response = await Client.GetAsync(
+ "http://localhost/ApiExplorerResponseTypeWithAttribute/GetVoid");
+
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject>(body);
+
+ // Assert
+ var description = Assert.Single(result);
+ Assert.Equal(4, description.SupportedResponseFormats.Count);
+ Assert.Equal(4, description.SupportedResponseFormats
+ .Where(respData => respData.ResponseType == type && respData.StatusCode == 200)
+ .Count());
+ Assert.Single(description.SupportedResponseFormats, frmt => frmt.MediaType == "text/json");
+ Assert.Single(description.SupportedResponseFormats, frmt => frmt.MediaType == "application/json");
+ Assert.Single(description.SupportedResponseFormats, frmt => frmt.MediaType == "text/xml");
+ Assert.Single(description.SupportedResponseFormats, frmt => frmt.MediaType == "application/xml");
+ }
+
+ [Fact]
+ public async Task ApiExplorer_ResponseType_DifferentOnAttributeThanReturnType()
+ {
+ // Arrange
+ var type = "ApiExplorerWebSite.Customer";
+
+ // Act
+ var response = await Client.GetAsync(
+ "http://localhost/ApiExplorerResponseTypeWithAttribute/GetProduct");
+
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject>(body);
+
+ // Assert
+ var description = Assert.Single(result);
+ Assert.Equal(4, description.SupportedResponseFormats.Count);
+ Assert.Equal(4, description.SupportedResponseFormats
+ .Where(respData => respData.ResponseType == type && respData.StatusCode == 200)
+ .Count());
+ Assert.Single(description.SupportedResponseFormats, frmt => frmt.MediaType == "text/json");
+ Assert.Single(description.SupportedResponseFormats, frmt => frmt.MediaType == "application/json");
+ Assert.Single(description.SupportedResponseFormats, frmt => frmt.MediaType == "text/xml");
+ Assert.Single(description.SupportedResponseFormats, frmt => frmt.MediaType == "application/xml");
}
[Theory]
- [InlineData("GetVoid", "ApiExplorerWebSite.Customer")]
[InlineData("GetObject", "ApiExplorerWebSite.Product")]
[InlineData("GetIActionResult", "System.String")]
- [InlineData("GetProduct", "ApiExplorerWebSite.Customer")]
[InlineData("GetTask", "System.Int32")]
public async Task ApiExplorer_ResponseType_KnownWithAttribute(string action, string type)
{
@@ -466,24 +526,74 @@ public async Task ApiExplorer_ResponseType_KnownWithAttribute(string action, str
// Assert
var description = Assert.Single(result);
- Assert.Equal(type, description.ResponseType);
+ var responseFormat = Assert.Single(description.SupportedResponseFormats);
+ Assert.Equal(type, responseFormat.ResponseType);
+ Assert.Equal("application/json", responseFormat.MediaType);
+ Assert.Equal(200, responseFormat.StatusCode);
}
- [Theory]
- [InlineData("Controller", "ApiExplorerWebSite.Product")]
- [InlineData("Action", "ApiExplorerWebSite.Customer")]
- public async Task ApiExplorer_ResponseType_OverrideOnAction(string action, string type)
+ [Fact]
+ public async Task ApiExplorer_ResponseType_InheritingFromController()
{
- // Arrange & Act
+ // Arrange
+ var type = "ApiExplorerWebSite.Product";
+ var errorType = "ApiExplorerWebSite.ErrorInfo";
+
+ // Act
var response = await Client.GetAsync(
- "http://localhost/ApiExplorerResponseTypeOverrideOnAction/" + action);
+ "http://localhost/ApiExplorerResponseTypeOverrideOnAction/Controller");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject>(body);
// Assert
var description = Assert.Single(result);
- Assert.Equal(type, description.ResponseType);
+ Assert.Equal(2, description.SupportedResponseFormats.Count);
+ var responseFormat = Assert.Single(description.SupportedResponseFormats.Where(
+ respData => respData.ResponseType == type));
+ Assert.Equal(type, responseFormat.ResponseType);
+ Assert.Equal(200, responseFormat.StatusCode);
+ Assert.Equal("application/json", responseFormat.MediaType);
+
+ responseFormat = Assert.Single(description.SupportedResponseFormats.Where(
+ respData => respData.ResponseType == errorType));
+ Assert.Equal(errorType, responseFormat.ResponseType);
+ Assert.Equal(500, responseFormat.StatusCode);
+ Assert.Equal("application/json", responseFormat.MediaType);
+ }
+
+ [Fact]
+ public async Task ApiExplorer_ResponseType_OverrideOnAction()
+ {
+ // Arrange
+ var type = "ApiExplorerWebSite.Customer";
+ var errorType = "ApiExplorerWebSite.ErrorInfoOverride"; // type overriding the one specified on the controller
+
+ // Act
+ var response = await Client.GetAsync(
+ "http://localhost/ApiExplorerResponseTypeOverrideOnAction/Action");
+
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject>(body);
+
+ // Assert
+ var description = Assert.Single(result);
+ Assert.Equal(8, description.SupportedResponseFormats.Count);
+ var formats = description.SupportedResponseFormats
+ .Where(respData => respData.ResponseType == type && respData.StatusCode == 200);
+ Assert.Equal(4, formats.Count());
+ Assert.Single(formats, frmt => frmt.MediaType == "text/json");
+ Assert.Single(formats, frmt => frmt.MediaType == "application/json");
+ Assert.Single(formats, frmt => frmt.MediaType == "text/xml");
+ Assert.Single(formats, frmt => frmt.MediaType == "application/xml");
+
+ formats = description.SupportedResponseFormats
+ .Where(respData => respData.ResponseType == errorType && respData.StatusCode == 500);
+ Assert.Equal(4, formats.Count());
+ Assert.Single(formats, frmt => frmt.MediaType == "text/json");
+ Assert.Single(formats, frmt => frmt.MediaType == "application/json");
+ Assert.Single(formats, frmt => frmt.MediaType == "text/xml");
+ Assert.Single(formats, frmt => frmt.MediaType == "application/xml");
}
[ConditionalFact]
@@ -729,8 +839,6 @@ private class ApiExplorerData
public string RelativePath { get; set; }
- public string ResponseType { get; set; }
-
public List SupportedResponseFormats { get; } = new List();
}
@@ -762,6 +870,10 @@ private class ApiExplorerResponseData
public string MediaType { get; set; }
public string FormatterType { get; set; }
+
+ public string ResponseType { get; set; }
+
+ public int StatusCode { get; set; }
}
}
}
\ No newline at end of file
diff --git a/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs b/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs
index db16c34f60..8ae1f037ba 100644
--- a/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs
+++ b/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs
@@ -52,8 +52,7 @@ private ApiExplorerData CreateSerializableData(ApiDescription description)
{
GroupName = description.GroupName,
HttpMethod = description.HttpMethod,
- RelativePath = description.RelativePath,
- ResponseType = description.ResponseType?.FullName,
+ RelativePath = description.RelativePath
};
foreach (var parameter in description.ParameterDescriptions)
@@ -82,8 +81,10 @@ private ApiExplorerData CreateSerializableData(ApiDescription description)
{
var responseData = new ApiExplorerResponseData()
{
- FormatterType = response.Formatter.GetType().FullName,
- MediaType = response.MediaType.ToString(),
+ FormatterType = response.Formatter?.GetType().FullName,
+ MediaType = response.MediaType,
+ StatusCode = response.StatusCode,
+ ResponseType = response.ResponseType?.FullName
};
data.SupportedResponseFormats.Add(responseData);
@@ -103,8 +104,6 @@ private class ApiExplorerData
public string RelativePath { get; set; }
- public string ResponseType { get; set; }
-
public List SupportedResponseFormats { get; } = new List();
}
@@ -136,6 +135,10 @@ private class ApiExplorerResponseData
public string MediaType { get; set; }
public string FormatterType { get; set; }
+
+ public string ResponseType { get; set; }
+
+ public int StatusCode { get; set; }
}
}
}
\ No newline at end of file
diff --git a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeOverrideOnActionController.cs b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeOverrideOnActionController.cs
index 8c6a946710..0b7d4bbce0 100644
--- a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeOverrideOnActionController.cs
+++ b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeOverrideOnActionController.cs
@@ -6,6 +6,7 @@
namespace ApiExplorerWebSite
{
[Produces("application/json", Type = typeof(Product))]
+ [ProducesResponseType(typeof(ErrorInfo), 500)]
[Route("ApiExplorerResponseTypeOverrideOnAction")]
public class ApiExplorerResponseTypeOverrideOnActionController : Controller
{
@@ -16,9 +17,12 @@ public void GetController()
[HttpGet("Action")]
[Produces(typeof(Customer))]
+ [ProducesResponseType(typeof(ErrorInfoOverride), 500)] // overriding the type specified on the server
public object GetAction()
{
return null;
}
}
+
+ public class ErrorInfoOverride { }
}
\ No newline at end of file
diff --git a/test/WebSites/ApiExplorerWebSite/Models/ErrorInfo.cs b/test/WebSites/ApiExplorerWebSite/Models/ErrorInfo.cs
new file mode 100644
index 0000000000..d8f9c2b536
--- /dev/null
+++ b/test/WebSites/ApiExplorerWebSite/Models/ErrorInfo.cs
@@ -0,0 +1,10 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace ApiExplorerWebSite
+{
+ public class ErrorInfo
+ {
+ public string Message { get; set; }
+ }
+}