Added new attribute ProducesResponseTypeAttribute to enable ApiExplorer to expose response type and StatusCode.
[Fixes #4101] StatusCode Metadata
This commit is contained in:
parent
fb81a5e11e
commit
6e9a6a2db1
|
|
@ -43,23 +43,6 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
/// </summary>
|
||||
public string RelativePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets <see cref="ModelMetadata"/> for the <see cref="ResponseType"/> or null.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Will be null if <see cref="ResponseType"/> is null.
|
||||
/// </remarks>
|
||||
public ModelMetadata ResponseModelMetadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the CLR data type of the response or null.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Will be null if the action returns no response, or if the response type is unclear. Use
|
||||
/// <c>ProducesAttribute</c> on an action method to specify a response type.
|
||||
/// </remarks>
|
||||
public Type ResponseType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of possible formats for a response.
|
||||
/// </summary>
|
||||
|
|
@ -76,6 +59,6 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
/// Will be empty if the action returns no response, or if the response type is unclear. Use
|
||||
/// <c>ProducesAttribute</c> on an action method to specify a response type.
|
||||
/// </remarks>
|
||||
public IList<ApiResponseFormat> SupportedResponseFormats { get; } = new List<ApiResponseFormat>();
|
||||
public IList<ApiResponseType> SupportedResponseTypes { get; } = new List<ApiResponseType>();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// 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 Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
|
@ -6,18 +6,18 @@ using Microsoft.AspNetCore.Mvc.Formatters;
|
|||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a possible format for the body of a response.
|
||||
/// Possible format for an <see cref="ApiResponseType"/>.
|
||||
/// </summary>
|
||||
public class ApiResponseFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// The formatter used to output this response.
|
||||
/// Gets or sets the formatter used to output this response.
|
||||
/// </summary>
|
||||
public IOutputFormatter Formatter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The media type of the response.
|
||||
/// Gets or sets the media type of the response.
|
||||
/// </summary>
|
||||
public string MediaType { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
// 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 System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||
{
|
||||
/// <summary>
|
||||
/// Possible type of the response body which is formatted by <see cref="ApiResponseFormats"/>.
|
||||
/// </summary>
|
||||
public class ApiResponseType
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the response formats supported by this type.
|
||||
/// </summary>
|
||||
public IList<ApiResponseFormat> ApiResponseFormats { get; set; } = new List<ApiResponseFormat>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets <see cref="ModelBinding.ModelMetadata"/> for the <see cref="Type"/> or null.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Will be null if <see cref="Type"/> is null or void.
|
||||
/// </remarks>
|
||||
public ModelMetadata ModelMetadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the CLR data type of the response or null.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Will be null if the action returns no response, or if the response type is unclear. Use
|
||||
/// <see cref="ProducesAttribute"/> or <see cref="ProducesResponseTypeAttribute"/> on an action method
|
||||
/// to specify a response type.
|
||||
/// </remarks>
|
||||
public Type Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP response status code.
|
||||
/// </summary>
|
||||
public int StatusCode { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ using System.Linq;
|
|||
using System.Reflection;
|
||||
#endif
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
|
@ -112,37 +113,20 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
// Void /Task object/IActionResult will result in no data.
|
||||
var declaredReturnType = GetDeclaredReturnType(action);
|
||||
|
||||
// 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 apiResponseTypes = GetApiResponseTypes(action, responseMetadataAttributes, runtimeReturnType);
|
||||
foreach (var apiResponseType in apiResponseTypes)
|
||||
{
|
||||
// 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.SupportedResponseTypes.Add(apiResponseType);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
|
@ -364,13 +348,24 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
return results;
|
||||
}
|
||||
|
||||
private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
|
||||
private IReadOnlyList<ApiResponseType> GetApiResponseTypes(
|
||||
ControllerActionDescriptor action,
|
||||
IApiResponseMetadataProvider[] responseMetadataAttributes,
|
||||
Type type)
|
||||
{
|
||||
var results = new List<ApiResponseFormat>();
|
||||
var results = new List<ApiResponseType>();
|
||||
|
||||
// Build list of all possible return types (and status codes) for an action.
|
||||
var objectTypes = new Dictionary<int, Type>();
|
||||
|
||||
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.
|
||||
var contentTypes = new MediaTypeCollection();
|
||||
|
|
@ -379,6 +374,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
foreach (var metadataAttribute in responseMetadataAttributes)
|
||||
{
|
||||
metadataAttribute.SetContentTypes(contentTypes);
|
||||
|
||||
if (metadataAttribute.Type != null)
|
||||
{
|
||||
objectTypes[metadataAttribute.StatusCode] = metadataAttribute.Type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -387,28 +387,53 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
contentTypes.Add((string)null);
|
||||
}
|
||||
|
||||
foreach (var contentType in contentTypes)
|
||||
{
|
||||
foreach (var formatter in _outputFormatters)
|
||||
{
|
||||
var responseFormatMetadataProvider = formatter as IApiResponseFormatMetadataProvider;
|
||||
if (responseFormatMetadataProvider != null)
|
||||
{
|
||||
var supportedTypes = responseFormatMetadataProvider.GetSupportedContentTypes(contentType, type);
|
||||
var responseTypeMetadataProviders = _outputFormatters.OfType<IApiResponseTypeMetadataProvider>();
|
||||
|
||||
if (supportedTypes != null)
|
||||
foreach (var objectType in objectTypes)
|
||||
{
|
||||
if (objectType.Value == typeof(void))
|
||||
{
|
||||
results.Add(new ApiResponseType()
|
||||
{
|
||||
StatusCode = objectType.Key,
|
||||
Type = objectType.Value
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var apiResponseType = new ApiResponseType()
|
||||
{
|
||||
Type = objectType.Value,
|
||||
StatusCode = objectType.Key,
|
||||
ModelMetadata = _modelMetadataProvider.GetMetadataForType(objectType.Value)
|
||||
};
|
||||
|
||||
foreach (var contentType in contentTypes)
|
||||
{
|
||||
foreach (var responseTypeMetadataProvider in responseTypeMetadataProviders)
|
||||
{
|
||||
var formatterSupportedContentTypes = responseTypeMetadataProvider.GetSupportedContentTypes(
|
||||
contentType,
|
||||
objectType.Value);
|
||||
|
||||
if (formatterSupportedContentTypes == null)
|
||||
{
|
||||
foreach (var supportedType in supportedTypes)
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var formatterSupportedContentType in formatterSupportedContentTypes)
|
||||
{
|
||||
apiResponseType.ApiResponseFormats.Add(new ApiResponseFormat()
|
||||
{
|
||||
results.Add(new ApiResponseFormat()
|
||||
{
|
||||
Formatter = formatter,
|
||||
MediaType = supportedType,
|
||||
});
|
||||
}
|
||||
Formatter = (IOutputFormatter)responseTypeMetadataProvider,
|
||||
MediaType = formatterSupportedContentType,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.Add(apiResponseType);
|
||||
}
|
||||
|
||||
return results;
|
||||
|
|
@ -445,28 +470,8 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
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.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -7,15 +7,21 @@ using Microsoft.AspNetCore.Mvc.Formatters;
|
|||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface IApiResponseMetadataProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Optimistic return type of the action.
|
||||
/// Gets the optimistic return type of the action.
|
||||
/// </summary>
|
||||
Type Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the HTTP status code of the response.
|
||||
/// </summary>
|
||||
int StatusCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Configures a collection of allowed content types which can be produced by the action.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
/// An <see cref="Formatters.IOutputFormatter"/> should implement this interface to expose metadata information
|
||||
/// to an <c>IApiDescriptionProvider</c>.
|
||||
/// </remarks>
|
||||
public interface IApiResponseFormatMetadataProvider
|
||||
public interface IApiResponseTypeMetadataProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a filtered list of content types which are supported by the <see cref="Formatters.IOutputFormatter"/>
|
||||
|
|
@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
/// <summary>
|
||||
/// Writes an object to the output stream.
|
||||
/// </summary>
|
||||
public abstract class OutputFormatter : IOutputFormatter, IApiResponseFormatMetadataProvider
|
||||
public abstract class OutputFormatter : IOutputFormatter, IApiResponseTypeMetadataProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the mutable collection of media type elements supported by
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
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,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
|
||||
public MediaTypeCollection ContentTypes { get; set; }
|
||||
|
||||
public int StatusCode => StatusCodes.Status200OK;
|
||||
|
||||
public override void OnResultExecuting(ResultExecutingContext context)
|
||||
{
|
||||
if (context == null)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the type of the value and status code returned by the action.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||
public class ProducesResponseTypeAttribute : Attribute, IApiResponseMetadataProvider, IFilterMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="ProducesResponseTypeAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The <see cref="Type"/> of object that is going to be written in the response.</param>
|
||||
/// <param name="statusCode">HTTP response status code</param>
|
||||
public ProducesResponseTypeAttribute(Type type, int statusCode)
|
||||
{
|
||||
if (type == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
}
|
||||
|
||||
Type = type;
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the value returned by an action.
|
||||
/// </summary>
|
||||
public Type Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP status code of the response.
|
||||
/// </summary>
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
void IApiResponseMetadataProvider.SetContentTypes(MediaTypeCollection contentTypes)
|
||||
{
|
||||
// Users are supposed to use the 'Produces' attribute to set the content types that an action can support.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ using Microsoft.AspNetCore.Mvc.Routing;
|
|||
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;
|
||||
|
|
@ -218,7 +217,7 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
}
|
||||
|
||||
// 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")]
|
||||
|
|
@ -374,8 +373,9 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
Assert.Equal(typeof(Product), description.ResponseType);
|
||||
Assert.NotNull(description.ResponseModelMetadata);
|
||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.Equal(typeof(Product), responseType.Type);
|
||||
Assert.NotNull(responseType.ModelMetadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -389,8 +389,9 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
Assert.Equal(typeof(Product), description.ResponseType);
|
||||
Assert.NotNull(description.ResponseModelMetadata);
|
||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.Equal(typeof(Product), responseType.Type);
|
||||
Assert.NotNull(responseType.ModelMetadata);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -410,9 +411,182 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
Assert.Null(description.ResponseType);
|
||||
Assert.Null(description.ResponseModelMetadata);
|
||||
Assert.Empty(description.SupportedResponseFormats);
|
||||
Assert.Empty(description.SupportedResponseTypes);
|
||||
}
|
||||
|
||||
public static TheoryData ReturnsActionResultWithProducesAndProducesContentTypeData
|
||||
{
|
||||
get
|
||||
{
|
||||
var filterDescriptors = new List<FilterDescriptor>()
|
||||
{
|
||||
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<Type, string, List<FilterDescriptor>>
|
||||
{
|
||||
{
|
||||
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<FilterDescriptor> filterDescriptors)
|
||||
{
|
||||
// Arrange
|
||||
var action = CreateActionDescriptor(methodName, controllerType);
|
||||
action.FilterDescriptors = filterDescriptors;
|
||||
var expectedMediaTypes = new[] { "application/json", "text/json" };
|
||||
|
||||
// Act
|
||||
var descriptions = GetApiDescriptions(action);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
Assert.Equal(3, description.SupportedResponseTypes.Count);
|
||||
|
||||
Assert.Collection(
|
||||
description.SupportedResponseTypes.OrderBy(responseType => responseType.StatusCode),
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(200, responseType.StatusCode);
|
||||
Assert.Equal(typeof(Customer), responseType.Type);
|
||||
Assert.NotNull(responseType.ModelMetadata);
|
||||
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(400, responseType.StatusCode);
|
||||
Assert.Equal(typeof(BadData), responseType.Type);
|
||||
Assert.NotNull(responseType.ModelMetadata);
|
||||
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(500, responseType.StatusCode);
|
||||
Assert.Equal(typeof(ErrorDetails), responseType.Type);
|
||||
Assert.NotNull(responseType.ModelMetadata);
|
||||
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||
});
|
||||
}
|
||||
|
||||
public static TheoryData<Type, string, List<FilterDescriptor>> ReturnsVoidOrTaskWithProducesContentTypeData
|
||||
{
|
||||
get
|
||||
{
|
||||
var filterDescriptors = new List<FilterDescriptor>()
|
||||
{
|
||||
// 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(void), 204),
|
||||
FilterScope.Action),
|
||||
new FilterDescriptor(
|
||||
new ProducesResponseTypeAttribute(typeof(BadData), 400),
|
||||
FilterScope.Action),
|
||||
new FilterDescriptor(
|
||||
new ProducesResponseTypeAttribute(typeof(ErrorDetails), 500),
|
||||
FilterScope.Action)
|
||||
};
|
||||
|
||||
return new TheoryData<Type, string, List<FilterDescriptor>>
|
||||
{
|
||||
{
|
||||
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<FilterDescriptor> filterDescriptors)
|
||||
{
|
||||
// Arrange
|
||||
var action = CreateActionDescriptor(methodName, controllerType);
|
||||
action.FilterDescriptors = filterDescriptors;
|
||||
var expectedMediaTypes = new[] { "application/json", "text/json" };
|
||||
|
||||
// Act
|
||||
var descriptions = GetApiDescriptions(action);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
Assert.Equal(3, description.SupportedResponseTypes.Count);
|
||||
|
||||
Assert.Collection(
|
||||
description.SupportedResponseTypes.OrderBy(responseType => responseType.StatusCode),
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(void), responseType.Type);
|
||||
Assert.Equal(204, responseType.StatusCode);
|
||||
Assert.Null(responseType.ModelMetadata);
|
||||
Assert.Empty(responseType.ApiResponseFormats);
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(BadData), responseType.Type);
|
||||
Assert.Equal(400, responseType.StatusCode);
|
||||
Assert.NotNull(responseType.ModelMetadata);
|
||||
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(ErrorDetails), responseType.Type);
|
||||
Assert.Equal(500, responseType.StatusCode);
|
||||
Assert.NotNull(responseType.ModelMetadata);
|
||||
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -422,15 +596,19 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
{
|
||||
// Arrange
|
||||
var action = CreateActionDescriptor(methodName);
|
||||
var filter = new ProducesResponseTypeAttribute(typeof(void), statusCode: 204);
|
||||
action.FilterDescriptors = new List<FilterDescriptor>();
|
||||
action.FilterDescriptors.Add(new FilterDescriptor(filter, FilterScope.Action));
|
||||
|
||||
// Act
|
||||
var descriptions = GetApiDescriptions(action);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
Assert.Equal(typeof(void), description.ResponseType);
|
||||
Assert.Null(description.ResponseModelMetadata);
|
||||
Assert.Empty(description.SupportedResponseFormats);
|
||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.Equal(typeof(void), responseType.Type);
|
||||
Assert.Equal(204, responseType.StatusCode);
|
||||
Assert.Null(responseType.ModelMetadata);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -459,8 +637,15 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
Assert.Equal(typeof(Order), description.ResponseType);
|
||||
Assert.NotNull(description.ResponseModelMetadata);
|
||||
var responseTypes = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.NotNull(responseTypes.ModelMetadata);
|
||||
Assert.Equal(200, responseTypes.StatusCode);
|
||||
Assert.Equal(typeof(Order), responseTypes.Type);
|
||||
|
||||
foreach (var responseFormat in responseTypes.ApiResponseFormats)
|
||||
{
|
||||
Assert.StartsWith("text/", responseFormat.MediaType);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -468,18 +653,15 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
{
|
||||
// Arrange
|
||||
var action = CreateActionDescriptor(nameof(ReturnsProduct));
|
||||
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
|
||||
|
||||
// Act
|
||||
var descriptions = GetApiDescriptions(action);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
Assert.Collection(
|
||||
description.SupportedResponseFormats.OrderBy(f => f.MediaType.ToString()),
|
||||
f => Assert.Equal("application/json", f.MediaType.ToString()),
|
||||
f => Assert.Equal("application/xml", f.MediaType.ToString()),
|
||||
f => Assert.Equal("text/json", f.MediaType.ToString()),
|
||||
f => Assert.Equal("text/xml", f.MediaType.ToString()));
|
||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -487,7 +669,7 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
{
|
||||
// Arrange
|
||||
var action = CreateActionDescriptor(nameof(ReturnsProduct));
|
||||
|
||||
var expectedMediaTypes = new[] { "text/json", "text/xml" };
|
||||
action.FilterDescriptors = new List<FilterDescriptor>();
|
||||
action.FilterDescriptors.Add(new FilterDescriptor(new ContentTypeAttribute("text/*"), FilterScope.Action));
|
||||
|
||||
|
|
@ -496,10 +678,8 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
Assert.Collection(
|
||||
description.SupportedResponseFormats.OrderBy(f => f.MediaType.ToString()),
|
||||
f => Assert.Equal("text/json", f.MediaType.ToString()),
|
||||
f => Assert.Equal("text/xml", f.MediaType.ToString()));
|
||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -528,13 +708,12 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
|
||||
// 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 responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.Equal(typeof(Order), responseType.Type);
|
||||
Assert.NotNull(responseType.ModelMetadata);
|
||||
var apiResponseFormat = Assert.Single(
|
||||
responseType.ApiResponseFormats.Where(responseFormat => responseFormat.MediaType == "text/json"));
|
||||
Assert.Same(formatters[0], apiResponseFormat.Formatter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -1152,7 +1331,7 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
{
|
||||
action.MethodInfo = controllerType.GetMethod(
|
||||
methodName ?? "ReturnsObject",
|
||||
BindingFlags.Instance | BindingFlags.Public);
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
action.ControllerTypeInfo = controllerType.GetTypeInfo();
|
||||
action.BoundProperties = new List<ParameterDescriptor>();
|
||||
|
|
@ -1192,6 +1371,13 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
return action;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetSortedMediaTypes(ApiResponseType apiResponseType)
|
||||
{
|
||||
return apiResponseType.ApiResponseFormats
|
||||
.OrderBy(responseType => responseType.MediaType)
|
||||
.Select(responseType => responseType.MediaType);
|
||||
}
|
||||
|
||||
private object ReturnsObject()
|
||||
{
|
||||
return null;
|
||||
|
|
@ -1357,6 +1543,39 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
|
|
@ -1520,10 +1739,13 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
Assert.Equal(typeof(void).FullName, description.ResponseType);
|
||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
|
||||
Assert.Equal(204, responseType.StatusCode);
|
||||
Assert.Empty(responseType.ResponseFormats);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -427,7 +431,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
Assert.Null(description.ResponseType);
|
||||
Assert.Empty(description.SupportedResponseTypes);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -443,17 +447,63 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
Assert.Equal(type, description.ResponseType);
|
||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.Equal(200, responseType.StatusCode);
|
||||
Assert.Equal(type, responseType.ResponseType);
|
||||
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApiExplorer_ResponseType_KnownWithoutAttribute_ReturnVoid()
|
||||
{
|
||||
// Arrange
|
||||
var type = "ApiExplorerWebSite.Customer";
|
||||
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync(
|
||||
"http://localhost/ApiExplorerResponseTypeWithAttribute/GetVoid");
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.Equal(200, responseType.StatusCode);
|
||||
Assert.Equal(type, responseType.ResponseType);
|
||||
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApiExplorer_ResponseType_DifferentOnAttributeThanReturnType()
|
||||
{
|
||||
// Arrange
|
||||
var type = "ApiExplorerWebSite.Customer";
|
||||
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync(
|
||||
"http://localhost/ApiExplorerResponseTypeWithAttribute/GetProduct");
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.Equal(200, responseType.StatusCode);
|
||||
Assert.Equal(type, responseType.ResponseType);
|
||||
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||
}
|
||||
|
||||
[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 +516,83 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
Assert.Equal(type, description.ResponseType);
|
||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.Equal(type, responseType.ResponseType);
|
||||
Assert.Equal(200, responseType.StatusCode);
|
||||
var responseFormat = Assert.Single(responseType.ResponseFormats);
|
||||
Assert.Equal("application/json", responseFormat.MediaType);
|
||||
}
|
||||
|
||||
[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<List<ApiExplorerData>>(body);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
Assert.Equal(type, description.ResponseType);
|
||||
Assert.Equal(2, description.SupportedResponseTypes.Count);
|
||||
|
||||
Assert.Collection(
|
||||
description.SupportedResponseTypes.OrderBy(responseType => responseType.StatusCode),
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(type, responseType.ResponseType);
|
||||
Assert.Equal(200, responseType.StatusCode);
|
||||
var responseFormat = Assert.Single(responseType.ResponseFormats);
|
||||
Assert.Equal("application/json", responseFormat.MediaType);
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(errorType, responseType.ResponseType);
|
||||
Assert.Equal(500, responseType.StatusCode);
|
||||
var responseFormat = Assert.Single(responseType.ResponseFormats);
|
||||
Assert.Equal("application/json", responseFormat.MediaType);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApiExplorer_ResponseType_OverrideOnAction()
|
||||
{
|
||||
// Arrange
|
||||
var type = "ApiExplorerWebSite.Customer";
|
||||
// type overriding the one specified on the controller
|
||||
var errorType = "ApiExplorerWebSite.ErrorInfoOverride";
|
||||
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync(
|
||||
"http://localhost/ApiExplorerResponseTypeOverrideOnAction/Action");
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
Assert.Equal(2, description.SupportedResponseTypes.Count);
|
||||
|
||||
Assert.Collection(
|
||||
description.SupportedResponseTypes.OrderBy(responseType => responseType.StatusCode),
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(type, responseType.ResponseType);
|
||||
Assert.Equal(200, responseType.StatusCode);
|
||||
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(errorType, responseType.ResponseType);
|
||||
Assert.Equal(500, responseType.StatusCode);
|
||||
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||
});
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
|
|
@ -500,17 +609,17 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
|
||||
var formats = description.SupportedResponseFormats;
|
||||
Assert.Equal(4, formats.Count);
|
||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.Equal(4, responseType.ResponseFormats.Count);
|
||||
|
||||
var textXml = Assert.Single(formats, f => f.MediaType == "text/xml");
|
||||
var textXml = Assert.Single(responseType.ResponseFormats, f => f.MediaType == "text/xml");
|
||||
Assert.Equal(typeof(XmlDataContractSerializerOutputFormatter).FullName, textXml.FormatterType);
|
||||
var applicationXml = Assert.Single(formats, f => f.MediaType == "application/xml");
|
||||
var applicationXml = Assert.Single(responseType.ResponseFormats, f => f.MediaType == "application/xml");
|
||||
Assert.Equal(typeof(XmlDataContractSerializerOutputFormatter).FullName, applicationXml.FormatterType);
|
||||
|
||||
var textJson = Assert.Single(formats, f => f.MediaType == "text/json");
|
||||
var textJson = Assert.Single(responseType.ResponseFormats, f => f.MediaType == "text/json");
|
||||
Assert.Equal(typeof(JsonOutputFormatter).FullName, textJson.FormatterType);
|
||||
var applicationJson = Assert.Single(formats, f => f.MediaType == "application/json");
|
||||
var applicationJson = Assert.Single(responseType.ResponseFormats, f => f.MediaType == "application/json");
|
||||
Assert.Equal(typeof(JsonOutputFormatter).FullName, applicationJson.FormatterType);
|
||||
}
|
||||
|
||||
|
|
@ -526,13 +635,15 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
|
||||
var formats = description.SupportedResponseFormats;
|
||||
Assert.Equal(2, formats.Count);
|
||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.Equal(2, responseType.ResponseFormats.Count);
|
||||
|
||||
var applicationJson = Assert.Single(formats, f => f.MediaType == "application/json");
|
||||
var applicationJson = Assert.Single(
|
||||
responseType.ResponseFormats,
|
||||
format => format.MediaType == "application/json");
|
||||
Assert.Equal(typeof(JsonOutputFormatter).FullName, applicationJson.FormatterType);
|
||||
|
||||
var textJson = Assert.Single(formats, f => f.MediaType == "text/json");
|
||||
var textJson = Assert.Single(responseType.ResponseFormats, f => f.MediaType == "text/json");
|
||||
Assert.Equal(typeof(JsonOutputFormatter).FullName, textJson.FormatterType);
|
||||
}
|
||||
|
||||
|
|
@ -547,9 +658,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
|
||||
var formats = description.SupportedResponseFormats;
|
||||
Assert.Empty(formats);
|
||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
Assert.Empty(responseType.ResponseFormats);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
|
|
@ -572,9 +682,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
|
||||
var format = Assert.Single(description.SupportedResponseFormats);
|
||||
Assert.Equal(contentType, format.MediaType);
|
||||
Assert.Equal(formatterType, format.FormatterType);
|
||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||
var responseFormat = Assert.Single(responseType.ResponseFormats);
|
||||
Assert.Equal(contentType, responseFormat.MediaType);
|
||||
Assert.Equal(formatterType, responseFormat.FormatterType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -718,6 +829,13 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal(typeof(string).FullName, feedback.Type);
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetSortedMediaTypes(ApiExplorerResponseType apiResponseType)
|
||||
{
|
||||
return apiResponseType.ResponseFormats
|
||||
.OrderBy(format => format.MediaType)
|
||||
.Select(format => format.MediaType);
|
||||
}
|
||||
|
||||
// Used to serialize data between client and server
|
||||
private class ApiExplorerData
|
||||
{
|
||||
|
|
@ -729,9 +847,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
|
||||
public string RelativePath { get; set; }
|
||||
|
||||
public string ResponseType { get; set; }
|
||||
|
||||
public List<ApiExplorerResponseData> SupportedResponseFormats { get; } = new List<ApiExplorerResponseData>();
|
||||
public List<ApiExplorerResponseType> SupportedResponseTypes { get; } = new List<ApiExplorerResponseType>();
|
||||
}
|
||||
|
||||
// Used to serialize data between client and server
|
||||
|
|
@ -757,7 +873,17 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
}
|
||||
|
||||
// Used to serialize data between client and server
|
||||
private class ApiExplorerResponseData
|
||||
private class ApiExplorerResponseType
|
||||
{
|
||||
public IList<ApiExplorerResponseFormat> ResponseFormats { get; }
|
||||
= new List<ApiExplorerResponseFormat>();
|
||||
|
||||
public string ResponseType { get; set; }
|
||||
|
||||
public int StatusCode { get; set; }
|
||||
}
|
||||
|
||||
private class ApiExplorerResponseFormat
|
||||
{
|
||||
public string MediaType { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -52,8 +52,7 @@ namespace ApiExplorerWebSite
|
|||
{
|
||||
GroupName = description.GroupName,
|
||||
HttpMethod = description.HttpMethod,
|
||||
RelativePath = description.RelativePath,
|
||||
ResponseType = description.ResponseType?.FullName,
|
||||
RelativePath = description.RelativePath
|
||||
};
|
||||
|
||||
foreach (var parameter in description.ParameterDescriptions)
|
||||
|
|
@ -78,15 +77,24 @@ namespace ApiExplorerWebSite
|
|||
data.ParameterDescriptions.Add(parameterData);
|
||||
}
|
||||
|
||||
foreach (var response in description.SupportedResponseFormats)
|
||||
foreach (var response in description.SupportedResponseTypes)
|
||||
{
|
||||
var responseData = new ApiExplorerResponseData()
|
||||
var responseType = new ApiExplorerResponseType()
|
||||
{
|
||||
FormatterType = response.Formatter.GetType().FullName,
|
||||
MediaType = response.MediaType.ToString(),
|
||||
StatusCode = response.StatusCode,
|
||||
ResponseType = response.Type?.FullName
|
||||
};
|
||||
|
||||
data.SupportedResponseFormats.Add(responseData);
|
||||
foreach(var responseFormat in response.ApiResponseFormats)
|
||||
{
|
||||
responseType.ResponseFormats.Add(new ApiExplorerResponseFormat()
|
||||
{
|
||||
FormatterType = responseFormat.Formatter?.GetType().FullName,
|
||||
MediaType = responseFormat.MediaType
|
||||
});
|
||||
}
|
||||
|
||||
data.SupportedResponseTypes.Add(responseType);
|
||||
}
|
||||
|
||||
return data;
|
||||
|
|
@ -103,9 +111,7 @@ namespace ApiExplorerWebSite
|
|||
|
||||
public string RelativePath { get; set; }
|
||||
|
||||
public string ResponseType { get; set; }
|
||||
|
||||
public List<ApiExplorerResponseData> SupportedResponseFormats { get; } = new List<ApiExplorerResponseData>();
|
||||
public List<ApiExplorerResponseType> SupportedResponseTypes { get; } = new List<ApiExplorerResponseType>();
|
||||
}
|
||||
|
||||
// Used to serialize data between client and server
|
||||
|
|
@ -131,7 +137,17 @@ namespace ApiExplorerWebSite
|
|||
}
|
||||
|
||||
// Used to serialize data between client and server
|
||||
private class ApiExplorerResponseData
|
||||
private class ApiExplorerResponseType
|
||||
{
|
||||
public IList<ApiExplorerResponseFormat> ResponseFormats { get; }
|
||||
= new List<ApiExplorerResponseFormat>();
|
||||
|
||||
public string ResponseType { get; set; }
|
||||
|
||||
public int StatusCode { get; set; }
|
||||
}
|
||||
|
||||
private class ApiExplorerResponseFormat
|
||||
{
|
||||
public string MediaType { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||
namespace ApiExplorerWebSite
|
||||
{
|
||||
[Produces("application/json", Type = typeof(Product))]
|
||||
[ProducesResponseType(typeof(ErrorInfo), 500)]
|
||||
[Route("ApiExplorerResponseTypeOverrideOnAction")]
|
||||
public class ApiExplorerResponseTypeOverrideOnActionController : Controller
|
||||
{
|
||||
|
|
@ -16,9 +17,17 @@ namespace ApiExplorerWebSite
|
|||
|
||||
[HttpGet("Action")]
|
||||
[Produces(typeof(Customer))]
|
||||
[ProducesResponseType(typeof(ErrorInfoOverride), 500)] // overriding the type specified on the server
|
||||
public object GetAction()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class ErrorInfo
|
||||
{
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
public class ErrorInfoOverride { }
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ namespace ApiExplorerWebSite
|
|||
public class ApiExplorerResponseTypeWithoutAttributeController : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(void), 204)]
|
||||
public void GetVoid()
|
||||
{
|
||||
}
|
||||
|
|
@ -45,6 +46,7 @@ namespace ApiExplorerWebSite
|
|||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(void), 204)]
|
||||
public Task GetTask()
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
|
|
|
|||
Loading…
Reference in New Issue