diff --git a/src/Microsoft.AspNet.Mvc.ApiExplorer/ApiDescription.cs b/src/Microsoft.AspNet.Mvc.ApiExplorer/ApiDescription.cs index f2b1a2ff60..584ba7c859 100644 --- a/src/Microsoft.AspNet.Mvc.ApiExplorer/ApiDescription.cs +++ b/src/Microsoft.AspNet.Mvc.ApiExplorer/ApiDescription.cs @@ -14,47 +14,37 @@ namespace Microsoft.AspNet.Mvc.ApiExplorer public class ApiDescription { /// - /// Creates a new instance of . - /// - public ApiDescription() - { - Properties = new Dictionary(); - ParameterDescriptions = new List(); - SupportedResponseFormats = new List(); - } - - /// - /// The for this api. + /// Gets or sets for this api. /// public ActionDescriptor ActionDescriptor { get; set; } /// - /// The group name for this api. + /// Gets or sets group name for this api. /// public string GroupName { get; set; } /// - /// 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. /// public string HttpMethod { get; set; } /// - /// The list of for this api. + /// Gets a list of for this api. /// - public IList ParameterDescriptions { get; private set; } + public IList ParameterDescriptions { get; } = new List(); /// - /// Stores arbitrary metadata properties associated with the . + /// Gets arbitrary metadata properties associated with the . /// - public IDictionary Properties { get; private set; } + public IDictionary Properties { get; } = new Dictionary(); /// - /// 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. /// public string RelativePath { get; set; } /// - /// The for the or null. + /// Gets or sets for the or null. /// /// /// Will be null if is null. @@ -62,7 +52,7 @@ public ApiDescription() public ModelMetadata ResponseModelMetadata { get; set; } /// - /// The CLR data type of the response or null. + /// 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 @@ -71,12 +61,21 @@ public ApiDescription() public Type ResponseType { get; set; } /// - /// A list of possible formats for a response. + /// Gets the list of possible formats for a response. + /// + /// + /// Will be empty 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 IList SupportedRequestFormats { get; } = new List(); + + /// + /// Gets the list of possible formats for a response. /// /// /// Will be empty 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 IList SupportedResponseFormats { get; private set; } + public IList SupportedResponseFormats { get; } = new List(); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ApiExplorer/ApiRequestFormat.cs b/src/Microsoft.AspNet.Mvc.ApiExplorer/ApiRequestFormat.cs new file mode 100644 index 0000000000..40321987de --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ApiExplorer/ApiRequestFormat.cs @@ -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 +{ + /// + /// A possible format for the body of a request. + /// + public class ApiRequestFormat + { + /// + /// The formatter used to read this request. + /// + public IInputFormatter Formatter { get; set; } + + /// + /// The media type of the request. + /// + public MediaTypeHeaderValue MediaType { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs b/src/Microsoft.AspNet.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs index ccfde0a0dd..88860b947b 100644 --- a/src/Microsoft.AspNet.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs @@ -25,6 +25,7 @@ namespace Microsoft.AspNet.Mvc.ApiExplorer /// public class DefaultApiDescriptionProvider : IApiDescriptionProvider { + private readonly IList _inputFormatters; private readonly IList _outputFormatters; private readonly IModelMetadataProvider _modelMetadataProvider; private readonly IInlineConstraintResolver _constraintResolver; @@ -41,6 +42,7 @@ public DefaultApiDescriptionProvider( IInlineConstraintResolver constraintResolver, IModelMetadataProvider modelMetadataProvider) { + _inputFormatters = optionsAccessor.Value.InputFormatters; _outputFormatters = optionsAccessor.Value.OutputFormatters; _constraintResolver = constraintResolver; _modelMetadataProvider = modelMetadataProvider; @@ -102,6 +104,7 @@ private ApiDescription CreateApiDescription( 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. @@ -126,19 +129,27 @@ private ApiDescription CreateApiDescription( 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 GetParameters(ApiParameterContext context) { // First, get parameters from the model-binding/parameter-binding side of the world. @@ -302,6 +313,56 @@ private string GetRelativePath(RouteTemplate parsedTemplate) return string.Join("/", segments); } + private IReadOnlyList GetRequestFormats( + ControllerActionDescriptor action, + IApiRequestMetadataProvider[] requestMetadataAttributes, + Type type) + { + var results = new List(); + + // 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 List(); + if (requestMetadataAttributes != null) + { + foreach (var metadataAttribute in requestMetadataAttributes) + { + metadataAttribute.SetContentTypes(contentTypes); + } + } + + if (contentTypes.Count == 0) + { + contentTypes.Add(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 GetResponseFormats( ControllerActionDescriptor action, IApiResponseMetadataProvider[] responseMetadataAttributes, @@ -419,6 +480,23 @@ private Type GetRuntimeReturnType(Type declaredReturnType, IApiResponseMetadataP 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() + .ToArray(); + } + private IApiResponseMetadataProvider[] GetResponseMetadataAttributes(ControllerActionDescriptor action) { if (action.FilterDescriptors == null) diff --git a/src/Microsoft.AspNet.Mvc.Core/ApiExplorer/IApiRequestFormatMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ApiExplorer/IApiRequestFormatMetadataProvider.cs new file mode 100644 index 0000000000..429725d72d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ApiExplorer/IApiRequestFormatMetadataProvider.cs @@ -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 +{ + /// + /// Provides metadata information about the request format to an IApiDescriptionProvider. + /// + /// + /// An should implement this interface to expose metadata information + /// to an IApiDescriptionProvider. + /// + public interface IApiRequestFormatMetadataProvider + { + /// + /// Gets a filtered list of content types which are supported by the + /// for the and . + /// + /// + /// The content type for which the supported content types are desired, or null if any content + /// type can be used. + /// + /// + /// The for which the supported content types are desired. + /// + /// Content types which are supported by the . + IReadOnlyList GetSupportedContentTypes( + MediaTypeHeaderValue contentType, + Type objectType); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ApiExplorer/IApiRequestMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ApiExplorer/IApiRequestMetadataProvider.cs new file mode 100644 index 0000000000..ee0dca5ac8 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ApiExplorer/IApiRequestMetadataProvider.cs @@ -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 System.Collections.Generic; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNet.Mvc.ApiExplorer +{ + /// + /// Provides a a set of possible content types than can be consumed by the action. + /// + public interface IApiRequestMetadataProvider + { + /// + /// Configures a collection of allowed content types which can be consumed by the action. + /// + void SetContentTypes(IList contentTypes); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ApiExplorer/IApiResponseFormatMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ApiExplorer/IApiResponseFormatMetadataProvider.cs index 80c8c088ed..f07f1d8a48 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ApiExplorer/IApiResponseFormatMetadataProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ApiExplorer/IApiResponseFormatMetadataProvider.cs @@ -18,7 +18,7 @@ public interface IApiResponseFormatMetadataProvider { /// /// Gets a filtered list of content types which are supported by the - /// for the and . + /// for the and . /// /// /// The content type for which the supported content types are desired, or null if any content diff --git a/src/Microsoft.AspNet.Mvc.Core/ConsumesAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/ConsumesAttribute.cs index 83ef895467..1bb7d4ae8c 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ConsumesAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ConsumesAttribute.cs @@ -6,6 +6,7 @@ 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.Net.Http.Headers; @@ -16,7 +17,11 @@ namespace Microsoft.AspNet.Mvc /// Specifies the allowed content types which can be used to select the action based on request's content-type. /// [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; @@ -184,5 +189,15 @@ private List GetContentTypes(string firstArg, string[] arg return contentTypes; } + + /// + public void SetContentTypes(IList contentTypes) + { + contentTypes.Clear(); + foreach (var contentType in ContentTypes) + { + contentTypes.Add(contentType); + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs index a6fd215ef0..a8b7d07965 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.ApiExplorer; using Microsoft.AspNet.Mvc.Core; using Microsoft.Net.Http.Headers; @@ -15,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.Formatters /// /// Reads an object from the request body. /// - public abstract class InputFormatter : IInputFormatter + public abstract class InputFormatter : IInputFormatter, IApiRequestFormatMetadataProvider { /// /// Returns UTF8 Encoding without BOM and throws on invalid bytes. @@ -147,5 +148,41 @@ protected Encoding SelectCharacterEncoding(InputFormatterContext context) return null; } + + /// + public IReadOnlyList GetSupportedContentTypes(MediaTypeHeaderValue 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 ? new List(SupportedMediaTypes) : null; + } + else + { + List 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) + { + if (mediaType.IsSubsetOf(contentType)) + { + if (mediaTypes == null) + { + mediaTypes = new List(); + } + + mediaTypes.Add(mediaType); + } + } + + return mediaTypes; + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs b/test/Microsoft.AspNet.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs index ca8d57080c..a25601f9a9 100644 --- a/test/Microsoft.AspNet.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.Description public class DefaultApiDescriptionProviderTest { [Fact] - public void GetApiDescription_IgnoresNonReflectedActionDescriptor() + public void GetApiDescription_IgnoresNonControllerActionDescriptor() { // Arrange var action = new ActionDescriptor(); @@ -470,13 +470,12 @@ public void GetApiDescription_IncludesResponseFormats() // 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] @@ -493,11 +492,10 @@ public void GetApiDescription_IncludesResponseFormats_FilteredByAttribute() // 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] @@ -513,7 +511,7 @@ public void GetApiDescription_IncludesResponseFormats_FilteredByType() action.FilterDescriptors = new List(); 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)); @@ -522,7 +520,7 @@ public void GetApiDescription_IncludesResponseFormats_FilteredByType() formatters[1].SupportedTypes.Add(typeof(Product)); // Act - var descriptions = GetApiDescriptions(action, formatters); + var descriptions = GetApiDescriptions(action, outputFormatters: formatters); // Assert var description = Assert.Single(descriptions); @@ -535,6 +533,87 @@ public void GetApiDescription_IncludesResponseFormats_FilteredByType() 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(); + 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(); + 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() { @@ -985,19 +1064,20 @@ public void GetApiDescription_WithControllerProperties_Merges_ParameterDescripti Assert.Equal(typeof(string), comments.Type); } - private IReadOnlyList GetApiDescriptions(ActionDescriptor action) - { - return GetApiDescriptions(action, CreateFormatters()); - } - private IReadOnlyList GetApiDescriptions( ActionDescriptor action, - List formatters) + List inputFormatters = null, + List 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); } @@ -1023,13 +1103,31 @@ private IReadOnlyList GetApiDescriptions( return new ReadOnlyCollection(context.Results); } - private List CreateFormatters() + private List CreateInputFormatters() { // Include some default formatters that look reasonable, some tests will override this. - var formatters = new List() + var formatters = new List() { - 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 CreateOutputFormatters() + { + // Include some default formatters that look reasonable, some tests will override this. + var formatters = new List() + { + new MockOutputFormatter(), + new MockOutputFormatter(), }; formatters[0].SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json")); @@ -1355,7 +1453,33 @@ public class Person public int Id { get; set; } } - private class MockFormatter : OutputFormatter + private class MockInputFormatter : InputFormatter + { + public List SupportedTypes { get; } = new List(); + + public override Task 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 SupportedTypes { get; } = new List(); @@ -1381,7 +1505,11 @@ protected override bool CanWriteType(Type type) } } - private class ContentTypeAttribute : Attribute, IFilterMetadata, IApiResponseMetadataProvider + private class ContentTypeAttribute : + Attribute, + IFilterMetadata, + IApiResponseMetadataProvider, + IApiRequestMetadataProvider { public ContentTypeAttribute(string mediaType) { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ConsumesAttributeTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ConsumesAttributeTests.cs index ad6fc81fc6..d37788731b 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ConsumesAttributeTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ConsumesAttributeTests.cs @@ -9,6 +9,7 @@ using Microsoft.AspNet.Mvc.ActionConstraints; using Microsoft.AspNet.Mvc.Filters; using Microsoft.AspNet.Routing; +using Microsoft.Net.Http.Headers; using Moq; using Xunit; @@ -367,6 +368,28 @@ public void OnResourceExecuting_ForAContentTypeMatch_IsNoOp(string contentType) Assert.Null(resourceExecutingContext.Result); } + [Fact] + public void SetContentTypes_ClearsAndSetsContentTypes() + { + // Arrange + var attribute = new ConsumesAttribute("application/json", "text/json"); + + var contentTypes = new List() + { + MediaTypeHeaderValue.Parse("application/xml"), + MediaTypeHeaderValue.Parse("text/xml"), + }; + + // Act + attribute.SetContentTypes(contentTypes); + + // Assert + Assert.Collection( + contentTypes.OrderBy(t => t.ToString()), + t => Assert.Equal("application/xml", t.ToString()), + t => Assert.Equal("text/xml", t.ToString())); + } + private static RouteContext CreateRouteContext(string contentType = null, object routeValues = null) { var httpContext = new DefaultHttpContext(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/InputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/InputFormatterTest.cs index 066aa140f5..28da6e1f8b 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/InputFormatterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/InputFormatterTest.cs @@ -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 @@ public void XMLFormatter_CanRead_ReturnsFalseForUnsupportedMediaTypes(string req 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.ToString())); + } + + [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.ToString()), + c => Assert.Equal("text/xml", c.ToString())); + } + + [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(new MediaTypeHeaderValue("text/*"), typeof(string)); + + // Assert + Assert.Collection(results, c => Assert.Equal("text/xml", c.ToString())); + } + private class TestFormatter : InputFormatter { + public IList SupportedTypes { get; } = new List(); + + protected override bool CanReadType(Type type) + { + return SupportedTypes.Count == 0 ? true : SupportedTypes.Contains(type); + } + public override Task ReadRequestBodyAsync(InputFormatterContext context) { throw new NotImplementedException();