Fixes #3818 - Support Consumes in ApiExplorer
This change adds a list of ApiRequestFormat objects to ApiDescription object which include the content type and formatter for each supported content type which can be understood by the action. Computation is aware of the [Consumes] attribute via the IApiRequestMetadataProvider metadata interface, and aware of Input Formatters via the new IApiRequestFormatMetadataProvider interface. This algorithm is essentially the same as what we do for produces/output-formatters. We iterate the filters and ask them what content types they think are supported. Then we cross check that list with the formatters, and ask them which from that list are supported. If no [Consumes] filters are used, the formatters will include everything they support by default. This feature and data is only available when an action has a [FromBody] parameter, which will naturally exclude actions that handle GET or DELETE and don't process the body.
This commit is contained in:
parent
32bb324886
commit
420f442487
|
|
@ -1,30 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<<<<<<< HEAD
|
||||
<Project ToolsVersion="14.0.24720" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.24720</VisualStudioVersion>
|
||||
=======
|
||||
<Project ToolsVersion="14.0.24711" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.24711</VisualStudioVersion>
|
||||
>>>>>>> CR feedback
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<<<<<<< HEAD
|
||||
<ProjectGuid>45F6B3B6-D114-4D77-84D6-561B3957F341</ProjectGuid>
|
||||
=======
|
||||
<ProjectGuid>e26979a8-56fb-41f7-8da5-a49a570acd39</ProjectGuid>
|
||||
>>>>>>> CR feedback
|
||||
<RootNamespace>MvcSubAreaSample.Web</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
>>>>>>> CR feedback
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -14,47 +14,37 @@ namespace Microsoft.AspNet.Mvc.ApiExplorer
|
|||
public class ApiDescription
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ApiDescription"/>.
|
||||
/// </summary>
|
||||
public ApiDescription()
|
||||
{
|
||||
Properties = new Dictionary<object, object>();
|
||||
ParameterDescriptions = new List<ApiParameterDescription>();
|
||||
SupportedResponseFormats = new List<ApiResponseFormat>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ActionDescriptor"/> for this api.
|
||||
/// Gets or sets <see cref="ActionDescriptor"/> for this api.
|
||||
/// </summary>
|
||||
public ActionDescriptor ActionDescriptor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The group name for this api.
|
||||
/// Gets or sets group name for this api.
|
||||
/// </summary>
|
||||
public string GroupName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The supported HTTP method for this api, or null if all HTTP methods are supported.
|
||||
/// Gets or sets the supported HTTP method for this api, or null if all HTTP methods are supported.
|
||||
/// </summary>
|
||||
public string HttpMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of <see cref="ApiParameterDescription"/> for this api.
|
||||
/// Gets a list of <see cref="ApiParameterDescription"/> for this api.
|
||||
/// </summary>
|
||||
public IList<ApiParameterDescription> ParameterDescriptions { get; private set; }
|
||||
public IList<ApiParameterDescription> ParameterDescriptions { get; } = new List<ApiParameterDescription>();
|
||||
|
||||
/// <summary>
|
||||
/// Stores arbitrary metadata properties associated with the <see cref="ApiDescription"/>.
|
||||
/// Gets arbitrary metadata properties associated with the <see cref="ApiDescription"/>.
|
||||
/// </summary>
|
||||
public IDictionary<object, object> Properties { get; private set; }
|
||||
public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>();
|
||||
|
||||
/// <summary>
|
||||
/// The relative url path template (relative to application root) for this api.
|
||||
/// Gets or sets relative url path template (relative to application root) for this api.
|
||||
/// </summary>
|
||||
public string RelativePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ModelMetadata"/> for the <see cref="ResponseType"/> or null.
|
||||
/// Gets or sets <see cref="ModelMetadata"/> for the <see cref="ResponseType"/> or null.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Will be null if <see cref="ResponseType"/> is null.
|
||||
|
|
@ -62,7 +52,7 @@ namespace Microsoft.AspNet.Mvc.ApiExplorer
|
|||
public ModelMetadata ResponseModelMetadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The CLR data type of the response or null.
|
||||
/// 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
|
||||
|
|
@ -71,12 +61,21 @@ namespace Microsoft.AspNet.Mvc.ApiExplorer
|
|||
public Type ResponseType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of possible formats for a response.
|
||||
/// Gets the list of possible formats for a response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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; private set; }
|
||||
public IList<ApiRequestFormat> SupportedRequestFormats { get; } = new List<ApiRequestFormat>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of possible formats for a response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// 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.AspNet.Mvc.Formatters;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ApiExplorer
|
||||
{
|
||||
/// <summary>
|
||||
/// A possible format for the body of a request.
|
||||
/// </summary>
|
||||
public class ApiRequestFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// The formatter used to read this request.
|
||||
/// </summary>
|
||||
public IInputFormatter Formatter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The media type of the request.
|
||||
/// </summary>
|
||||
public string MediaType { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ namespace Microsoft.AspNet.Mvc.ApiExplorer
|
|||
/// </summary>
|
||||
public class DefaultApiDescriptionProvider : IApiDescriptionProvider
|
||||
{
|
||||
private readonly IList<IInputFormatter> _inputFormatters;
|
||||
private readonly IList<IOutputFormatter> _outputFormatters;
|
||||
private readonly IModelMetadataProvider _modelMetadataProvider;
|
||||
private readonly IInlineConstraintResolver _constraintResolver;
|
||||
|
|
@ -40,6 +41,7 @@ namespace Microsoft.AspNet.Mvc.ApiExplorer
|
|||
IInlineConstraintResolver constraintResolver,
|
||||
IModelMetadataProvider modelMetadataProvider)
|
||||
{
|
||||
_inputFormatters = optionsAccessor.Value.InputFormatters;
|
||||
_outputFormatters = optionsAccessor.Value.OutputFormatters;
|
||||
_constraintResolver = constraintResolver;
|
||||
_modelMetadataProvider = modelMetadataProvider;
|
||||
|
|
@ -101,6 +103,7 @@ namespace Microsoft.AspNet.Mvc.ApiExplorer
|
|||
apiDescription.ParameterDescriptions.Add(parameter);
|
||||
}
|
||||
|
||||
var requestMetadataAttributes = GetRequestMetadataAttributes(action);
|
||||
var responseMetadataAttributes = GetResponseMetadataAttributes(action);
|
||||
|
||||
// We only provide response info if we can figure out a type that is a user-data type.
|
||||
|
|
@ -125,19 +128,27 @@ namespace Microsoft.AspNet.Mvc.ApiExplorer
|
|||
|
||||
apiDescription.ResponseModelMetadata = _modelMetadataProvider.GetMetadataForType(runtimeReturnType);
|
||||
|
||||
var formats = GetResponseFormats(
|
||||
action,
|
||||
responseMetadataAttributes,
|
||||
runtimeReturnType);
|
||||
|
||||
var formats = GetResponseFormats(action, responseMetadataAttributes, runtimeReturnType);
|
||||
foreach (var format in formats)
|
||||
{
|
||||
apiDescription.SupportedResponseFormats.Add(format);
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
apiDescription.SupportedRequestFormats.Add(format);
|
||||
}
|
||||
}
|
||||
|
||||
return apiDescription;
|
||||
}
|
||||
|
||||
private IList<ApiParameterDescription> GetParameters(ApiParameterContext context)
|
||||
{
|
||||
// First, get parameters from the model-binding/parameter-binding side of the world.
|
||||
|
|
@ -301,6 +312,56 @@ namespace Microsoft.AspNet.Mvc.ApiExplorer
|
|||
return string.Join("/", segments);
|
||||
}
|
||||
|
||||
private IReadOnlyList<ApiRequestFormat> GetRequestFormats(
|
||||
ControllerActionDescriptor action,
|
||||
IApiRequestMetadataProvider[] requestMetadataAttributes,
|
||||
Type type)
|
||||
{
|
||||
var results = new List<ApiRequestFormat>();
|
||||
|
||||
// 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();
|
||||
if (requestMetadataAttributes != null)
|
||||
{
|
||||
foreach (var metadataAttribute in requestMetadataAttributes)
|
||||
{
|
||||
metadataAttribute.SetContentTypes(contentTypes);
|
||||
}
|
||||
}
|
||||
|
||||
if (contentTypes.Count == 0)
|
||||
{
|
||||
contentTypes.Add((string)null);
|
||||
}
|
||||
|
||||
foreach (var contentType in contentTypes)
|
||||
{
|
||||
foreach (var formatter in _inputFormatters)
|
||||
{
|
||||
var requestFormatMetadataProvider = formatter as IApiRequestFormatMetadataProvider;
|
||||
if (requestFormatMetadataProvider != null)
|
||||
{
|
||||
var supportedTypes = requestFormatMetadataProvider.GetSupportedContentTypes(contentType, type);
|
||||
|
||||
if (supportedTypes != null)
|
||||
{
|
||||
foreach (var supportedType in supportedTypes)
|
||||
{
|
||||
results.Add(new ApiRequestFormat()
|
||||
{
|
||||
Formatter = formatter,
|
||||
MediaType = supportedType,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
|
||||
ControllerActionDescriptor action,
|
||||
IApiResponseMetadataProvider[] responseMetadataAttributes,
|
||||
|
|
@ -418,6 +479,23 @@ namespace Microsoft.AspNet.Mvc.ApiExplorer
|
|||
return declaredReturnType;
|
||||
}
|
||||
|
||||
private IApiRequestMetadataProvider[] GetRequestMetadataAttributes(ControllerActionDescriptor action)
|
||||
{
|
||||
if (action.FilterDescriptors == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// This technique for enumerating filters will intentionally ignore any filter that is an IFilterFactory
|
||||
// while searching for a filter that implements IApiRequestMetadataProvider.
|
||||
//
|
||||
// The workaround for that is to implement the metadata interface on the IFilterFactory.
|
||||
return action.FilterDescriptors
|
||||
.Select(fd => fd.Filter)
|
||||
.OfType<IApiRequestMetadataProvider>()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private IApiResponseMetadataProvider[] GetResponseMetadataAttributes(ControllerActionDescriptor action)
|
||||
{
|
||||
if (action.FilterDescriptors == null)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
// 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.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ApiExplorer
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides metadata information about the request format to an <c>IApiDescriptionProvider</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An <see cref="Formatters.IInputFormatter"/> should implement this interface to expose metadata information
|
||||
/// to an <c>IApiDescriptionProvider</c>.
|
||||
/// </remarks>
|
||||
public interface IApiRequestFormatMetadataProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a filtered list of content types which are supported by the <see cref="Formatters.IInputFormatter"/>
|
||||
/// for the <paramref name="objectType"/> and <paramref name="contentType"/>.
|
||||
/// </summary>
|
||||
/// <param name="contentType">
|
||||
/// The content type for which the supported content types are desired, or <c>null</c> if any content
|
||||
/// type can be used.
|
||||
/// </param>
|
||||
/// <param name="objectType">
|
||||
/// The <see cref="Type"/> for which the supported content types are desired.
|
||||
/// </param>
|
||||
/// <returns>Content types which are supported by the <see cref="Formatters.IInputFormatter"/>.</returns>
|
||||
IReadOnlyList<string> GetSupportedContentTypes(
|
||||
string contentType,
|
||||
Type objectType);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// 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.AspNet.Mvc.Formatters;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ApiExplorer
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a a set of possible content types than can be consumed by the action.
|
||||
/// </summary>
|
||||
public interface IApiRequestMetadataProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures a collection of allowed content types which can be consumed by the action.
|
||||
/// </summary>
|
||||
/// <param name="contentTypes">The <see cref="MediaTypeCollection"/></param>
|
||||
void SetContentTypes(MediaTypeCollection contentTypes);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ namespace Microsoft.AspNet.Mvc.ApiExplorer
|
|||
{
|
||||
/// <summary>
|
||||
/// Gets a filtered list of content types which are supported by the <see cref="Formatters.IOutputFormatter"/>
|
||||
/// for the <paramref name="declaredType"/> and <paramref name="contentType"/>.
|
||||
/// for the <paramref name="objectType"/> and <paramref name="contentType"/>.
|
||||
/// </summary>
|
||||
/// <param name="contentType">
|
||||
/// The content type for which the supported content types are desired, or <c>null</c> if any content
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using Microsoft.AspNet.Mvc.Abstractions;
|
||||
using Microsoft.AspNet.Mvc.ActionConstraints;
|
||||
using Microsoft.AspNet.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
using Microsoft.AspNet.Mvc.Filters;
|
||||
using Microsoft.AspNet.Mvc.Formatters;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
|
|
@ -18,7 +18,11 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// Specifies the allowed content types which can be used to select the action based on request's content-type.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||
public class ConsumesAttribute : Attribute, IResourceFilter, IConsumesActionConstraint
|
||||
public class ConsumesAttribute :
|
||||
Attribute,
|
||||
IResourceFilter,
|
||||
IConsumesActionConstraint,
|
||||
IApiRequestMetadataProvider
|
||||
{
|
||||
public static readonly int ConsumesActionConstraintOrder = 200;
|
||||
|
||||
|
|
@ -207,5 +211,15 @@ namespace Microsoft.AspNet.Mvc
|
|||
|
||||
return contentTypes;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetContentTypes(MediaTypeCollection contentTypes)
|
||||
{
|
||||
contentTypes.Clear();
|
||||
foreach (var contentType in ContentTypes)
|
||||
{
|
||||
contentTypes.Add(contentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ using System.Linq;
|
|||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
|
@ -16,7 +17,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
|
|||
/// <summary>
|
||||
/// Reads an object from the request body.
|
||||
/// </summary>
|
||||
public abstract class InputFormatter : IInputFormatter
|
||||
public abstract class InputFormatter : IInputFormatter, IApiRequestFormatMetadataProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns UTF8 Encoding without BOM and throws on invalid bytes.
|
||||
|
|
@ -159,5 +160,43 @@ namespace Microsoft.AspNet.Mvc.Formatters
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> GetSupportedContentTypes(string contentType, Type objectType)
|
||||
{
|
||||
if (!CanReadType(objectType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (contentType == null)
|
||||
{
|
||||
// If contentType is null, then any type we support is valid.
|
||||
return SupportedMediaTypes.Count > 0 ? SupportedMediaTypes : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var parsedContentType = new MediaType(contentType);
|
||||
List<string> mediaTypes = null;
|
||||
|
||||
// Confirm this formatter supports a more specific media type than requested e.g. OK if "text/*"
|
||||
// requested and formatter supports "text/plain". Treat contentType like it came from an Content-Type header.
|
||||
foreach (var mediaType in SupportedMediaTypes)
|
||||
{
|
||||
var parsedMediaType = new MediaType(mediaType);
|
||||
if (parsedMediaType.IsSubsetOf(parsedContentType))
|
||||
{
|
||||
if (mediaTypes == null)
|
||||
{
|
||||
mediaTypes = new List<string>();
|
||||
}
|
||||
|
||||
mediaTypes.Add(mediaType);
|
||||
}
|
||||
}
|
||||
|
||||
return mediaTypes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
|
|||
throw new ArgumentNullException(nameof(item));
|
||||
}
|
||||
|
||||
Add(item.ToString());
|
||||
Add(item?.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Mvc.Description
|
|||
public class DefaultApiDescriptionProviderTest
|
||||
{
|
||||
[Fact]
|
||||
public void GetApiDescription_IgnoresNonReflectedActionDescriptor()
|
||||
public void GetApiDescription_IgnoresNonControllerActionDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var action = new ActionDescriptor();
|
||||
|
|
@ -471,13 +471,12 @@ namespace Microsoft.AspNet.Mvc.Description
|
|||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
Assert.Equal(4, description.SupportedResponseFormats.Count);
|
||||
|
||||
var formats = description.SupportedResponseFormats;
|
||||
Assert.Single(formats, f => f.MediaType.ToString() == "text/json");
|
||||
Assert.Single(formats, f => f.MediaType.ToString() == "application/json");
|
||||
Assert.Single(formats, f => f.MediaType.ToString() == "text/xml");
|
||||
Assert.Single(formats, f => f.MediaType.ToString() == "application/xml");
|
||||
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()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -494,11 +493,10 @@ namespace Microsoft.AspNet.Mvc.Description
|
|||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
Assert.Equal(2, description.SupportedResponseFormats.Count);
|
||||
|
||||
var formats = description.SupportedResponseFormats;
|
||||
Assert.Single(formats, f => f.MediaType.ToString() == "text/json");
|
||||
Assert.Single(formats, f => f.MediaType.ToString() == "text/xml");
|
||||
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()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -514,7 +512,7 @@ namespace Microsoft.AspNet.Mvc.Description
|
|||
action.FilterDescriptors = new List<FilterDescriptor>();
|
||||
action.FilterDescriptors.Add(new FilterDescriptor(filter, FilterScope.Action));
|
||||
|
||||
var formatters = CreateFormatters();
|
||||
var formatters = CreateOutputFormatters();
|
||||
|
||||
// This will just format Order
|
||||
formatters[0].SupportedTypes.Add(typeof(Order));
|
||||
|
|
@ -523,7 +521,7 @@ namespace Microsoft.AspNet.Mvc.Description
|
|||
formatters[1].SupportedTypes.Add(typeof(Product));
|
||||
|
||||
// Act
|
||||
var descriptions = GetApiDescriptions(action, formatters);
|
||||
var descriptions = GetApiDescriptions(action, outputFormatters: formatters);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
|
|
@ -536,6 +534,87 @@ namespace Microsoft.AspNet.Mvc.Description
|
|||
Assert.Same(formatters[0], formats[0].Formatter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiDescription_RequestFormatsEmpty_WithNoBodyParameter()
|
||||
{
|
||||
// Arrange
|
||||
var action = CreateActionDescriptor(nameof(AcceptsProduct));
|
||||
|
||||
// Act
|
||||
var descriptions = GetApiDescriptions(action);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
Assert.Empty(description.SupportedRequestFormats);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiDescription_IncludesRequestFormats()
|
||||
{
|
||||
// Arrange
|
||||
var action = CreateActionDescriptor(nameof(AcceptsProduct_Body));
|
||||
|
||||
// Act
|
||||
var descriptions = GetApiDescriptions(action);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
Assert.Collection(
|
||||
description.SupportedRequestFormats.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()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiDescription_IncludesRequestFormats_FilteredByAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var action = CreateActionDescriptor(nameof(AcceptsProduct_Body));
|
||||
|
||||
action.FilterDescriptors = new List<FilterDescriptor>();
|
||||
action.FilterDescriptors.Add(new FilterDescriptor(new ContentTypeAttribute("text/*"), FilterScope.Action));
|
||||
|
||||
// Act
|
||||
var descriptions = GetApiDescriptions(action);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
Assert.Collection(
|
||||
description.SupportedRequestFormats.OrderBy(f => f.MediaType.ToString()),
|
||||
f => Assert.Equal("text/json", f.MediaType.ToString()),
|
||||
f => Assert.Equal("text/xml", f.MediaType.ToString()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiDescription_IncludesRequestFormats_FilteredByType()
|
||||
{
|
||||
// Arrange
|
||||
var action = CreateActionDescriptor(nameof(AcceptsProduct_Body));
|
||||
|
||||
action.FilterDescriptors = new List<FilterDescriptor>();
|
||||
action.FilterDescriptors.Add(new FilterDescriptor(new ContentTypeAttribute("text/*"), FilterScope.Action));
|
||||
|
||||
var formatters = CreateInputFormatters();
|
||||
|
||||
// This will just format Order
|
||||
formatters[0].SupportedTypes.Add(typeof(Order));
|
||||
|
||||
// This will just format Product
|
||||
formatters[1].SupportedTypes.Add(typeof(Product));
|
||||
|
||||
// Act
|
||||
var descriptions = GetApiDescriptions(action, inputFormatters: formatters);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(descriptions);
|
||||
|
||||
var format = Assert.Single(description.SupportedRequestFormats);
|
||||
Assert.Equal("text/xml", format.MediaType.ToString());
|
||||
Assert.Same(formatters[1], format.Formatter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiDescription_ParameterDescription_ModelBoundParameter()
|
||||
{
|
||||
|
|
@ -986,19 +1065,20 @@ namespace Microsoft.AspNet.Mvc.Description
|
|||
Assert.Equal(typeof(string), comments.Type);
|
||||
}
|
||||
|
||||
private IReadOnlyList<ApiDescription> GetApiDescriptions(ActionDescriptor action)
|
||||
{
|
||||
return GetApiDescriptions(action, CreateFormatters());
|
||||
}
|
||||
|
||||
private IReadOnlyList<ApiDescription> GetApiDescriptions(
|
||||
ActionDescriptor action,
|
||||
List<MockFormatter> formatters)
|
||||
List<MockInputFormatter> inputFormatters = null,
|
||||
List<MockOutputFormatter> outputFormatters = null)
|
||||
{
|
||||
var context = new ApiDescriptionProviderContext(new ActionDescriptor[] { action });
|
||||
|
||||
var options = new MvcOptions();
|
||||
foreach (var formatter in formatters)
|
||||
foreach (var formatter in inputFormatters ?? CreateInputFormatters())
|
||||
{
|
||||
options.InputFormatters.Add(formatter);
|
||||
}
|
||||
|
||||
foreach (var formatter in outputFormatters ?? CreateOutputFormatters())
|
||||
{
|
||||
options.OutputFormatters.Add(formatter);
|
||||
}
|
||||
|
|
@ -1024,13 +1104,31 @@ namespace Microsoft.AspNet.Mvc.Description
|
|||
return new ReadOnlyCollection<ApiDescription>(context.Results);
|
||||
}
|
||||
|
||||
private List<MockFormatter> CreateFormatters()
|
||||
private List<MockInputFormatter> CreateInputFormatters()
|
||||
{
|
||||
// Include some default formatters that look reasonable, some tests will override this.
|
||||
var formatters = new List<MockFormatter>()
|
||||
var formatters = new List<MockInputFormatter>()
|
||||
{
|
||||
new MockFormatter(),
|
||||
new MockFormatter(),
|
||||
new MockInputFormatter(),
|
||||
new MockInputFormatter(),
|
||||
};
|
||||
|
||||
formatters[0].SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
|
||||
formatters[0].SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/json"));
|
||||
|
||||
formatters[1].SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/xml"));
|
||||
formatters[1].SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
|
||||
|
||||
return formatters;
|
||||
}
|
||||
|
||||
private List<MockOutputFormatter> CreateOutputFormatters()
|
||||
{
|
||||
// Include some default formatters that look reasonable, some tests will override this.
|
||||
var formatters = new List<MockOutputFormatter>()
|
||||
{
|
||||
new MockOutputFormatter(),
|
||||
new MockOutputFormatter(),
|
||||
};
|
||||
|
||||
formatters[0].SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
|
||||
|
|
@ -1356,7 +1454,33 @@ namespace Microsoft.AspNet.Mvc.Description
|
|||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
private class MockFormatter : OutputFormatter
|
||||
private class MockInputFormatter : InputFormatter
|
||||
{
|
||||
public List<Type> SupportedTypes { get; } = new List<Type>();
|
||||
|
||||
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override bool CanReadType(Type type)
|
||||
{
|
||||
if (SupportedTypes.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (type == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return SupportedTypes.Contains(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MockOutputFormatter : OutputFormatter
|
||||
{
|
||||
public List<Type> SupportedTypes { get; } = new List<Type>();
|
||||
|
||||
|
|
@ -1382,7 +1506,11 @@ namespace Microsoft.AspNet.Mvc.Description
|
|||
}
|
||||
}
|
||||
|
||||
private class ContentTypeAttribute : Attribute, IFilterMetadata, IApiResponseMetadataProvider
|
||||
private class ContentTypeAttribute :
|
||||
Attribute,
|
||||
IFilterMetadata,
|
||||
IApiResponseMetadataProvider,
|
||||
IApiRequestMetadataProvider
|
||||
{
|
||||
public ContentTypeAttribute(string mediaType)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ using Microsoft.AspNet.Http.Internal;
|
|||
using Microsoft.AspNet.Mvc.Abstractions;
|
||||
using Microsoft.AspNet.Mvc.ActionConstraints;
|
||||
using Microsoft.AspNet.Mvc.Filters;
|
||||
using Microsoft.AspNet.Mvc.Formatters;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -367,6 +369,28 @@ namespace Microsoft.AspNet.Mvc
|
|||
Assert.Null(resourceExecutingContext.Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetContentTypes_ClearsAndSetsContentTypes()
|
||||
{
|
||||
// Arrange
|
||||
var attribute = new ConsumesAttribute("application/json", "text/json");
|
||||
|
||||
var contentTypes = new MediaTypeCollection()
|
||||
{
|
||||
MediaTypeHeaderValue.Parse("application/xml"),
|
||||
MediaTypeHeaderValue.Parse("text/xml"),
|
||||
};
|
||||
|
||||
// Act
|
||||
attribute.SetContentTypes(contentTypes);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
contentTypes.OrderBy(t => t),
|
||||
t => Assert.Equal("application/json", t),
|
||||
t => Assert.Equal("text/json", t));
|
||||
}
|
||||
|
||||
private static RouteContext CreateRouteContext(string contentType = null, object routeValues = null)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
// 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 System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http.Internal;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
|
|
@ -317,8 +319,78 @@ namespace Microsoft.AspNet.Mvc.Formatters
|
|||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSupportedContentTypes_UnsupportedObjectType_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new TestFormatter();
|
||||
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
|
||||
formatter.SupportedTypes.Add(typeof(string));
|
||||
|
||||
// Act
|
||||
var results = formatter.GetSupportedContentTypes(contentType: null, objectType: typeof(int));
|
||||
|
||||
// Assert
|
||||
Assert.Null(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSupportedContentTypes_SupportedObjectType_ReturnsContentTypes()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new TestFormatter();
|
||||
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
|
||||
formatter.SupportedTypes.Add(typeof(string));
|
||||
|
||||
// Act
|
||||
var results = formatter.GetSupportedContentTypes(contentType: null, objectType: typeof(string));
|
||||
|
||||
// Assert
|
||||
Assert.Collection(results, c => Assert.Equal("text/xml", c));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSupportedContentTypes_NullContentType_ReturnsAllContentTypes()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new TestFormatter();
|
||||
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/xml"));
|
||||
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
|
||||
|
||||
// Act
|
||||
var results = formatter.GetSupportedContentTypes(contentType: null, objectType: typeof(string));
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
results.OrderBy(c => c.ToString()),
|
||||
c => Assert.Equal("application/xml", c),
|
||||
c => Assert.Equal("text/xml", c));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSupportedContentTypes_NonNullContentType_FiltersContentTypes()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new TestFormatter();
|
||||
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/xml"));
|
||||
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
|
||||
|
||||
// Act
|
||||
var results = formatter.GetSupportedContentTypes("text/*", typeof(string));
|
||||
|
||||
// Assert
|
||||
Assert.Collection(results, c => Assert.Equal("text/xml", c));
|
||||
}
|
||||
|
||||
private class TestFormatter : InputFormatter
|
||||
{
|
||||
public IList<Type> SupportedTypes { get; } = new List<Type>();
|
||||
|
||||
protected override bool CanReadType(Type type)
|
||||
{
|
||||
return SupportedTypes.Count == 0 ? true : SupportedTypes.Contains(type);
|
||||
}
|
||||
|
||||
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
|
|
|||
Loading…
Reference in New Issue