Correct polarity of MediaTypeHeaderValue.IsSubsetOf()` checks and remove one conneg fallback

- aspnet/Mvc#3138 part 2/2
- request's Content-Type header must be a subset of what an `IInputFormatter` can consume
  - `[Consumes]` is similar
- what an `IOutputFormatter` produces must be a subset of the request's Accept header
  - `FormatFilter` and `ObjectResult` are similar
- `ObjectResult` no longer falls back to `Content-Type` header if no `Accept` value is acceptable
- left `WebApiCompatShim` code alone for consistency with down-level `System.Net.Http.Formatting`
- correct tests to match new behaviour
  - do not test `Accept` values containing a `charset` parameter; that case is not valid

WIP:
- four test failures; something about comparing media types w/ charset included
- why do some localization tests fail in VS?

nits:
- add `InputFormatterTests`
- add / update comments and doc comments
- correct xUnit attributes in `ActionResultTest`; odd it doesn't show up in command-line runs
This commit is contained in:
Doug Bunting 2015-10-13 15:18:11 -07:00
parent 2e2043f427
commit 03625c38af
17 changed files with 460 additions and 147 deletions

View File

@ -56,10 +56,10 @@ namespace Microsoft.AspNet.Mvc
MediaTypeHeaderValue requestContentType = null;
MediaTypeHeaderValue.TryParse(context.HttpContext.Request.ContentType, out requestContentType);
// Only execute if this is the last filter before calling the action.
// This ensures that we only run the filter which is closest to the action.
// Confirm the request's content type is more specific than a media type this action supports e.g. OK
// if client sent "text/plain" data and this action supports "text/*".
if (requestContentType != null &&
!ContentTypes.Any(contentType => contentType.IsSubsetOf(requestContentType)))
!ContentTypes.Any(contentType => requestContentType.IsSubsetOf(contentType)))
{
context.Result = new UnsupportedMediaTypeResult();
}
@ -102,7 +102,9 @@ namespace Microsoft.AspNet.Mvc
return !isActionWithoutConsumeConstraintPresent;
}
if (ContentTypes.Any(c => c.IsSubsetOf(requestContentType)))
// Confirm the request's content type is more specific than a media type this action supports e.g. OK
// if client sent "text/plain" data and this action supports "text/*".
if (ContentTypes.Any(contentType => requestContentType.IsSubsetOf(contentType)))
{
return true;
}

View File

@ -183,7 +183,7 @@ namespace Microsoft.AspNet.Mvc.Controllers
_cursor = new FilterCursor(_filters);
ActionContext.ModelState.MaxAllowedErrors = _maxModelValidationErrors;
await InvokeAllAuthorizationFiltersAsync();
// If Authorization Filters return a result, it's a short circuit because
@ -663,7 +663,7 @@ namespace Microsoft.AspNet.Mvc.Controllers
"Microsoft.AspNet.Mvc.BeforeActionMethod",
new
{
actionContext = ActionContext,
actionContext = ActionContext,
arguments = _actionExecutingContext.ActionArguments,
controller = _actionExecutingContext.Controller
});

View File

@ -13,7 +13,7 @@ using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.Mvc.Formatters
{
/// <summary>
/// A filter which will use the format value in the route data or query string to set the content type on an
/// A filter which will use the format value in the route data or query string to set the content type on an
/// <see cref="ObjectResult" /> returned from an action.
/// </summary>
public class FormatFilter : IFormatFilter, IResourceFilter, IResultFilter
@ -48,13 +48,13 @@ namespace Microsoft.AspNet.Mvc.Formatters
public MediaTypeHeaderValue ContentType { get; }
/// <summary>
/// <c>true</c> if the current <see cref="FormatFilter"/> is active and will execute.
/// <c>true</c> if the current <see cref="FormatFilter"/> is active and will execute.
/// </summary>
public bool IsActive { get; }
/// <summary>
/// As a <see cref="IResourceFilter"/>, this filter looks at the request and rejects it before going ahead if
/// 1. The format in the request doesnt match any format in the map.
/// 1. The format in the request does not match any format in the map.
/// 2. If there is a conflicting producesFilter.
/// </summary>
/// <param name="context">The <see cref="ResourceExecutingContext"/>.</param>
@ -67,7 +67,8 @@ namespace Microsoft.AspNet.Mvc.Formatters
if (!IsActive)
{
return; // no format specified by user, so the filter is muted
// no format specified by user, so the filter is muted
return;
}
if (ContentType == null)
@ -77,19 +78,22 @@ namespace Microsoft.AspNet.Mvc.Formatters
return;
}
// Determine media types this action supports.
var responseTypeFilters = context.Filters.OfType<IApiResponseMetadataProvider>();
var contentTypes = new List<MediaTypeHeaderValue>();
var supportedMediaTypes = new List<MediaTypeHeaderValue>();
foreach (var filter in responseTypeFilters)
{
filter.SetContentTypes(contentTypes);
filter.SetContentTypes(supportedMediaTypes);
}
if (contentTypes.Count != 0)
// Check if support is adequate for requested media type.
if (supportedMediaTypes.Count != 0)
{
// We need to check if the action can generate the content type the user asked for. If it cannot, exit
// here with not found result.
if (!contentTypes.Any(c => ContentType.IsSubsetOf(c)))
// We need to check if the action can generate the content type the user asked for. That is, treat the
// request's format and IApiResponseMetadataProvider-provided content types similarly to an Accept
// header and an output formatter's SupportedMediaTypes: Confirm action supports a more specific media
// type than requested e.g. OK if "text/*" requested and action supports "text/plain".
if (!supportedMediaTypes.Any(contentType => contentType.IsSubsetOf(ContentType)))
{
context.Result = new HttpNotFoundResult();
}

View File

@ -62,12 +62,17 @@ namespace Microsoft.AspNet.Mvc.Formatters
var contentType = context.HttpContext.Request.ContentType;
MediaTypeHeaderValue requestContentType;
if (!MediaTypeHeaderValue.TryParse(contentType, out requestContentType))
if (!MediaTypeHeaderValue.TryParse(contentType, out requestContentType) || requestContentType == null)
{
return false;
}
return SupportedMediaTypes.Any(supportedMediaType => supportedMediaType.IsSubsetOf(requestContentType));
// Confirm the request's content type is more specific than a media type this formatter supports e.g. OK if
// client sent "text/plain" data and this formatter supports "text/*".
return SupportedMediaTypes.Any(supportedMediaType =>
{
return requestContentType.IsSubsetOf(supportedMediaType);
});
}
/// <summary>

View File

@ -77,6 +77,8 @@ namespace Microsoft.AspNet.Mvc.Formatters
{
List<MediaTypeHeaderValue> 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 Accept header.
foreach (var mediaType in _supportedMediaTypes)
{
if (mediaType.IsSubsetOf(contentType))
@ -119,8 +121,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
{
var requestCharset = requestContentType.Charset;
encoding = SupportedEncodings.FirstOrDefault(
supportedEncoding =>
requestCharset.Equals(supportedEncoding.WebName));
supportedEncoding => requestCharset.Equals(supportedEncoding.WebName));
}
}
@ -151,10 +152,11 @@ namespace Microsoft.AspNet.Mvc.Formatters
}
else
{
// Since supportedMedia Type is going to be more specific check if supportedMediaType is a subset
// of the content type which is typically what we get on acceptHeader.
mediaType = SupportedMediaTypes
.FirstOrDefault(supportedMediaType => supportedMediaType.IsSubsetOf(contentType));
// Confirm this formatter supports a more specific media type than requested e.g. OK if "text/*"
// requested and formatter supports "text/plain". contentType is typically what we got in an Accept
// header.
mediaType = SupportedMediaTypes.FirstOrDefault(
supportedMediaType => supportedMediaType.IsSubsetOf(contentType));
}
if (mediaType != null)
@ -201,11 +203,11 @@ namespace Microsoft.AspNet.Mvc.Formatters
// Copy the media type as we don't want it to affect the next request
selectedMediaType = selectedMediaType.Copy();
// Not text-based media types will use an encoding/charset - binary formats just ignore it. We want to
// Note text-based media types will use an encoding/charset - binary formats just ignore it. We want to
// make this class work with media types that use encodings, and those that don't.
//
// The default implementation of SelectCharacterEncoding will read from the list of SupportedEncodings
// and will always choose a default encoding if any are supported. So, the only cases where the
// and will always choose a default encoding if any are supported. So, the only cases where the
// selectedEncoding can be null are:
//
// 1). No supported encodings - we assume this is a non-text format
@ -237,21 +239,17 @@ namespace Microsoft.AspNet.Mvc.Formatters
if (acceptCharsetHeaders != null && acceptCharsetHeaders.Count > 0)
{
var sortedAcceptCharsetHeaders = acceptCharsetHeaders
.Where(acceptCharset =>
acceptCharset.Quality != HeaderQuality.NoMatch)
.OrderByDescending(
m => m, StringWithQualityHeaderValueComparer.QualityComparer);
.Where(acceptCharset => acceptCharset.Quality != HeaderQuality.NoMatch)
.OrderByDescending(m => m, StringWithQualityHeaderValueComparer.QualityComparer);
foreach (var acceptCharset in sortedAcceptCharsetHeaders)
{
var charset = acceptCharset.Value;
if (!string.IsNullOrWhiteSpace(charset))
{
var encoding = SupportedEncodings.FirstOrDefault(
supportedEncoding =>
charset.Equals(supportedEncoding.WebName,
StringComparison.OrdinalIgnoreCase) ||
charset.Equals("*", StringComparison.Ordinal));
var encoding = SupportedEncodings.FirstOrDefault(supportedEncoding =>
charset.Equals(supportedEncoding.WebName, StringComparison.OrdinalIgnoreCase) ||
charset.Equals("*", StringComparison.Ordinal));
if (encoding != null)
{
return encoding;

View File

@ -86,17 +86,15 @@ namespace Microsoft.AspNet.Mvc
{
var logger = formatterContext.HttpContext.RequestServices.GetRequiredService<ILogger<ObjectResult>>();
// Check if any content-type was explicitly set (for example, via ProducesAttribute
// or Url path extension mapping). If yes, then ignore content-negotiation and use this content-type.
// Check if any content-type was explicitly set (for example, via ProducesAttribute
// or URL path extension mapping). If yes, then ignore content-negotiation and use this content-type.
if (ContentTypes.Count == 1)
{
logger.LogVerbose(
"Skipped content negotiation as content type '{ContentType}' is explicitly set for the response.",
ContentTypes[0]);
return SelectFormatterUsingAnyAcceptableContentType(formatterContext,
formatters,
ContentTypes);
return SelectFormatterUsingAnyAcceptableContentType(formatterContext, formatters, ContentTypes);
}
var sortedAcceptHeaderMediaTypes = GetSortedAcceptHeaderMediaTypes(formatterContext);
@ -106,11 +104,7 @@ namespace Microsoft.AspNet.Mvc
{
// Check if we have enough information to do content-negotiation, otherwise get the first formatter
// which can write the type.
MediaTypeHeaderValue requestContentType = null;
MediaTypeHeaderValue.TryParse(
formatterContext.HttpContext.Request.ContentType,
out requestContentType);
if (!sortedAcceptHeaderMediaTypes.Any() && requestContentType == null)
if (!sortedAcceptHeaderMediaTypes.Any())
{
logger.LogVerbose("No information found on request to perform content negotiation.");
@ -122,25 +116,12 @@ namespace Microsoft.AspNet.Mvc
//
// 1. Select based on sorted accept headers.
if (sortedAcceptHeaderMediaTypes.Any())
{
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
formatterContext,
formatters,
sortedAcceptHeaderMediaTypes);
}
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
formatterContext,
formatters,
sortedAcceptHeaderMediaTypes);
// 2. No formatter was found based on accept headers, fall back on request Content-Type header.
if (selectedFormatter == null && requestContentType != null)
{
selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(
formatterContext,
formatters,
new[] { requestContentType });
}
// 3. No formatter was found based on Accept and request Content-Type headers, so
// fallback on type based match.
// 2. No formatter was found based on Accept header. Fallback to type-based match.
if (selectedFormatter == null)
{
logger.LogVerbose("Could not find an output formatter based on content negotiation.");
@ -157,15 +138,15 @@ namespace Microsoft.AspNet.Mvc
if (sortedAcceptHeaderMediaTypes.Any())
{
// Filter and remove accept headers which cannot support any of the user specified content types.
// That is, confirm this result supports a more specific media type than requested e.g. OK if
// "text/*" requested and result supports "text/plain".
var filteredAndSortedAcceptHeaders = sortedAcceptHeaderMediaTypes
.Where(acceptHeader =>
ContentTypes.Any(contentType =>
contentType.IsSubsetOf(acceptHeader)));
.Where(acceptHeader => ContentTypes.Any(contentType => contentType.IsSubsetOf(acceptHeader)));
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
formatterContext,
formatters,
filteredAndSortedAcceptHeaders);
formatterContext,
formatters,
filteredAndSortedAcceptHeaders);
}
if (selectedFormatter == null)
@ -177,9 +158,9 @@ namespace Microsoft.AspNet.Mvc
// In any of these cases, if the user has specified content types,
// do a last effort to find a formatter which can write any of the user specified content type.
selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(
formatterContext,
formatters,
ContentTypes);
formatterContext,
formatters,
ContentTypes);
}
}
@ -202,9 +183,9 @@ namespace Microsoft.AspNet.Mvc
}
public virtual IOutputFormatter SelectFormatterUsingSortedAcceptHeaders(
OutputFormatterContext formatterContext,
IEnumerable<IOutputFormatter> formatters,
IEnumerable<MediaTypeHeaderValue> sortedAcceptHeaders)
OutputFormatterContext formatterContext,
IEnumerable<IOutputFormatter> formatters,
IEnumerable<MediaTypeHeaderValue> sortedAcceptHeaders)
{
IOutputFormatter selectedFormatter = null;
foreach (var contentType in sortedAcceptHeaders)
@ -212,8 +193,7 @@ namespace Microsoft.AspNet.Mvc
// Loop through each of the formatters and see if any one will support this
// mediaType Value.
selectedFormatter = formatters.FirstOrDefault(
formatter =>
formatter.CanWriteResult(formatterContext, contentType));
formatter => formatter.CanWriteResult(formatterContext, contentType));
if (selectedFormatter != null)
{
// we found our match.
@ -225,15 +205,14 @@ namespace Microsoft.AspNet.Mvc
}
public virtual IOutputFormatter SelectFormatterUsingAnyAcceptableContentType(
OutputFormatterContext formatterContext,
IEnumerable<IOutputFormatter> formatters,
IEnumerable<MediaTypeHeaderValue> acceptableContentTypes)
OutputFormatterContext formatterContext,
IEnumerable<IOutputFormatter> formatters,
IEnumerable<MediaTypeHeaderValue> acceptableContentTypes)
{
var selectedFormatter = formatters.FirstOrDefault(
formatter =>
acceptableContentTypes
.Any(contentType =>
formatter.CanWriteResult(formatterContext, contentType)));
formatter => acceptableContentTypes.Any(
contentType => formatter.CanWriteResult(formatterContext, contentType)));
return selectedFormatter;
}
@ -264,7 +243,7 @@ namespace Microsoft.AspNet.Mvc
if (respectAcceptHeader)
{
sortedAcceptHeaderMediaTypes = SortMediaTypeHeaderValues(incomingAcceptHeaderMediaTypes)
.Where(header => header.Quality != HeaderQuality.NoMatch);
.Where(header => header.Quality != HeaderQuality.NoMatch);
}
return sortedAcceptHeaderMediaTypes;
@ -286,9 +265,9 @@ namespace Microsoft.AspNet.Mvc
{
// Use OrderBy() instead of Array.Sort() as it performs fewer comparisons. In this case the comparisons
// are quite expensive so OrderBy() performs better.
return headerValues.OrderByDescending(headerValue =>
headerValue,
MediaTypeHeaderValueComparer.QualityComparer);
return headerValues.OrderByDescending(
headerValue => headerValue,
MediaTypeHeaderValueComparer.QualityComparer);
}
private IEnumerable<IOutputFormatter> GetDefaultFormatters(ActionContext context)
@ -302,7 +281,7 @@ namespace Microsoft.AspNet.Mvc
.GetRequiredService<IActionBindingContextAccessor>()
.ActionBindingContext;
// In scenarios where there is a resource filter which directly shortcircuits using an ObjectResult.
// In scenarios where there is a resource filter which directly short-circuits using an ObjectResult.
// actionBindingContext is not setup yet and is null.
if (actionBindingContext == null)
{

View File

@ -0,0 +1,292 @@
// 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.Threading.Tasks;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNet.Mvc.Formatters
{
public class InputFormatterTest
{
private class CatchAllFormatter : TestFormatter
{
public CatchAllFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("*/*"));
}
}
[Theory]
[InlineData("application/mathml-content+xml")]
[InlineData("application/mathml-presentation+xml")]
[InlineData("application/mathml+xml; undefined=ignored")]
[InlineData("application/octet-stream; padding=3")]
[InlineData("application/xml")]
[InlineData("application/xml-dtd; undefined=ignored")]
[InlineData("multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p")]
[InlineData("multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p; undefined=ignored")]
[InlineData("text/html")]
public void CatchAll_CanRead_ReturnsTrueForSupportedMediaTypes(string requestContentType)
{
// Arrange
var formatter = new CatchAllFormatter();
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = requestContentType;
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: new ModelStateDictionary(),
modelType: typeof(void));
// Act
var result = formatter.CanRead(context);
// Assert
Assert.True(result);
}
private class MultipartFormatter : TestFormatter
{
public MultipartFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("multipart/*"));
}
}
[Theory]
[InlineData("multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p")]
[InlineData("multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p; undefined=ignored")]
public void MultipartFormatter_CanRead_ReturnsTrueForSupportedMediaTypes(string requestContentType)
{
// Arrange
var formatter = new MultipartFormatter();
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = requestContentType;
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: new ModelStateDictionary(),
modelType: typeof(void));
// Act
var result = formatter.CanRead(context);
// Assert
Assert.True(result);
}
[Theory]
[InlineData("application/mathml-content+xml")]
[InlineData("application/mathml-presentation+xml")]
[InlineData("application/mathml+xml; undefined=ignored")]
[InlineData("application/octet-stream; padding=3")]
[InlineData("application/xml")]
[InlineData("application/xml-dtd; undefined=ignored")]
[InlineData("text/html")]
public void MultipartFormatter_CanRead_ReturnsFalseForUnsupportedMediaTypes(string requestContentType)
{
// Arrange
var formatter = new MultipartFormatter();
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = requestContentType;
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: new ModelStateDictionary(),
modelType: typeof(void));
// Act
var result = formatter.CanRead(context);
// Assert
Assert.False(result);
}
private class MultipartMixedFormatter : TestFormatter
{
public MultipartMixedFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("multipart/mixed"));
}
}
[Theory]
[InlineData("multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p")]
[InlineData("multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p; undefined=ignored")]
public void MultipartMixedFormatter_CanRead_ReturnsTrueForSupportedMediaTypes(string requestContentType)
{
// Arrange
var formatter = new MultipartMixedFormatter();
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = requestContentType;
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: new ModelStateDictionary(),
modelType: typeof(void));
// Act
var result = formatter.CanRead(context);
// Assert
Assert.True(result);
}
[Theory]
[InlineData("application/mathml-content+xml")]
[InlineData("application/mathml-presentation+xml")]
[InlineData("application/mathml+xml; undefined=ignored")]
[InlineData("application/octet-stream; padding=3")]
[InlineData("application/xml")]
[InlineData("application/xml-dtd; undefined=ignored")]
[InlineData("text/html")]
public void MultipartMixedFormatter_CanRead_ReturnsFalseForUnsupportedMediaTypes(string requestContentType)
{
// Arrange
var formatter = new MultipartMixedFormatter();
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = requestContentType;
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: new ModelStateDictionary(),
modelType: typeof(void));
// Act
var result = formatter.CanRead(context);
// Assert
Assert.False(result);
}
private class MathMLFormatter : TestFormatter
{
public MathMLFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/mathml-content+xml"));
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/mathml-presentation+xml"));
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/mathml+xml"));
}
}
[Theory]
[InlineData("application/mathml-content+xml")]
[InlineData("application/mathml-presentation+xml")]
[InlineData("application/mathml+xml; undefined=ignored")]
public void MathMLFormatter_CanRead_ReturnsTrueForSupportedMediaTypes(string requestContentType)
{
// Arrange
var formatter = new MathMLFormatter();
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = requestContentType;
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: new ModelStateDictionary(),
modelType: typeof(void));
// Act
var result = formatter.CanRead(context);
// Assert
Assert.True(result);
}
[Theory]
[InlineData("application/octet-stream; padding=3")]
[InlineData("application/xml")]
[InlineData("application/xml-dtd; undefined=ignored")]
[InlineData("multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p")]
[InlineData("multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p; undefined=ignored")]
[InlineData("text/html")]
public void MathMLFormatter_CanRead_ReturnsFalseForUnsupportedMediaTypes(string requestContentType)
{
// Arrange
var formatter = new MathMLFormatter();
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = requestContentType;
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: new ModelStateDictionary(),
modelType: typeof(void));
// Act
var result = formatter.CanRead(context);
// Assert
Assert.False(result);
}
// IsSubsetOf does not follow XML media type conventions. This formatter does not support "application/*+xml".
private class XmlFormatter : TestFormatter
{
public XmlFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/xml"));
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
}
}
[Theory]
[InlineData("application/xml")]
public void XMLFormatter_CanRead_ReturnsTrueForSupportedMediaTypes(string requestContentType)
{
// Arrange
var formatter = new XmlFormatter();
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = requestContentType;
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: new ModelStateDictionary(),
modelType: typeof(void));
// Act
var result = formatter.CanRead(context);
// Assert
Assert.True(result);
}
[Theory]
[InlineData("application/mathml-content+xml")]
[InlineData("application/mathml-presentation+xml")]
[InlineData("application/mathml+xml; undefined=ignored")]
[InlineData("application/octet-stream; padding=3")]
[InlineData("application/xml-dtd; undefined=ignored")]
[InlineData("multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p")]
[InlineData("multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p; undefined=ignored")]
[InlineData("text/html")]
public void XMLFormatter_CanRead_ReturnsFalseForUnsupportedMediaTypes(string requestContentType)
{
// Arrange
var formatter = new XmlFormatter();
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = requestContentType;
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: new ModelStateDictionary(),
modelType: typeof(void));
// Act
var result = formatter.CanRead(context);
// Assert
Assert.False(result);
}
private class TestFormatter : InputFormatter
{
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -12,7 +12,6 @@ using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.Formatters;
using Microsoft.AspNet.Mvc.Formatters.Xml;
using Microsoft.AspNet.Mvc.Infrastructure;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Testing.xunit;
@ -133,7 +132,7 @@ namespace Microsoft.AspNet.Mvc
var result = new ObjectResult(input);
result.ContentTypes = new List<MediaTypeHeaderValue>();
result.ContentTypes.Add(MediaTypeHeaderValue.Parse(expectedContentType));
result.ContentTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
// Act
await result.ExecuteResultAsync(actionContext);
@ -157,7 +156,7 @@ namespace Microsoft.AspNet.Mvc
// Set the content type property explicitly to a single value.
var result = new ObjectResult(input);
result.ContentTypes = new List<MediaTypeHeaderValue>();
result.ContentTypes.Add(MediaTypeHeaderValue.Parse(expectedContentType));
result.ContentTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
// Act
await result.ExecuteResultAsync(actionContext);
@ -338,23 +337,23 @@ namespace Microsoft.AspNet.Mvc
[InlineData("application/xml")]
[InlineData("application/custom")]
[InlineData("application/xml;q=1, application/custom;q=0.8")]
public void SelectFormatter_WithNoMatchingAcceptHeadersAndRequestContentType_PicksFormatterBasedOnObjectType
(string acceptHeader)
public void SelectFormatter_WithNoMatchingAcceptHeader_PicksFormatterBasedOnObjectType(string acceptHeader)
{
// For no accept headers,
// can write is called twice once for the request Content-Type and once for the type match pass.
// For each additional accept header, it is called once.
// For no Accept headers, CanWriteResult is called once for the type match pass. For each additional Accept
// header, it is called once.
// Arrange
var acceptHeaderCollection = string.IsNullOrEmpty(acceptHeader) ?
null : MediaTypeHeaderValue.ParseList(new[] { acceptHeader }).ToArray();
null :
MediaTypeHeaderValue.ParseList(new[] { acceptHeader }).ToArray();
var stream = new MemoryStream();
var httpResponse = new Mock<HttpResponse>();
httpResponse.SetupProperty<string>(o => o.ContentType);
httpResponse.SetupGet(r => r.Body).Returns(stream);
var actionContext = CreateMockActionContext(httpResponse.Object,
requestAcceptHeader: acceptHeader,
requestContentType: "application/xml");
var actionContext = CreateMockActionContext(
httpResponse.Object,
requestAcceptHeader: acceptHeader,
requestContentType: "application/text");
var input = "testInput";
var result = new ObjectResult(input);
var mockCountingFormatter = new Mock<IOutputFormatter>();
@ -365,35 +364,40 @@ namespace Microsoft.AspNet.Mvc
Object = input,
DeclaredType = typeof(string)
};
var mockCountingSupportedContentType = MediaTypeHeaderValue.Parse("application/text");
mockCountingFormatter.Setup(o => o.CanWriteResult(context,
It.Is<MediaTypeHeaderValue>(mth => mth == null)))
.Returns(true);
mockCountingFormatter.Setup(o => o.CanWriteResult(context, mockCountingSupportedContentType))
.Returns(true);
var requestContentType = MediaTypeHeaderValue.Parse("application/text");
mockCountingFormatter
.Setup(o => o.CanWriteResult(context, It.Is<MediaTypeHeaderValue>(mth => mth == null)))
.Returns(true);
mockCountingFormatter
.Setup(o => o.CanWriteResult(context, requestContentType))
.Returns(true);
// Set more than one formatters. The test output formatter throws on write.
result.Formatters = new List<IOutputFormatter>
{
new CannotWriteFormatter(),
mockCountingFormatter.Object,
};
{
new CannotWriteFormatter(),
mockCountingFormatter.Object,
};
// Act
var formatter = result.SelectFormatter(context, result.Formatters);
// Assert
Assert.Equal(mockCountingFormatter.Object, formatter);
// CanWriteResult is called once for the type-based match.
mockCountingFormatter.Verify(v => v.CanWriteResult(context, null), Times.Once());
// CanWriteResult is invoked for the following cases:
// 1. For each accept header present
// 2. Request Content-Type
// 3. Type based match
var callCount = (acceptHeaderCollection == null ? 0 : acceptHeaderCollection.Count()) + 2;
mockCountingFormatter.Verify(v => v.CanWriteResult(context,
It.IsNotIn<MediaTypeHeaderValue>(mockCountingSupportedContentType)),
Times.Exactly(callCount));
// CanWriteResult is never called for the request's Content-Type.
mockCountingFormatter.Verify(v => v.CanWriteResult(context, requestContentType), Times.Never());
// In total, CanWriteResult is invoked for the following cases:
// 1. For each Accept header present
// 2. Type-based match
var callCount = (acceptHeaderCollection == null ? 0 : acceptHeaderCollection.Count()) + 1;
mockCountingFormatter.Verify(
v => v.CanWriteResult(It.IsAny< OutputFormatterContext>(), It.IsAny<MediaTypeHeaderValue>()),
Times.Exactly(callCount));
}
[Fact]

View File

@ -22,10 +22,10 @@ namespace Microsoft.AspNet.Mvc.Formatters
[Theory]
[InlineData("application/json", true)]
[InlineData("application/*", true)]
[InlineData("*/*", true)]
[InlineData("application/*", false)]
[InlineData("*/*", false)]
[InlineData("text/json", true)]
[InlineData("text/*", true)]
[InlineData("text/*", false)]
[InlineData("text/xml", false)]
[InlineData("application/xml", false)]
[InlineData("", false)]

View File

@ -75,8 +75,8 @@ namespace Microsoft.AspNet.Mvc.Formatters
[Theory]
[InlineData("application/json-patch+json", true)]
[InlineData("application/json", false)]
[InlineData("application/*", true)]
[InlineData("*/*", true)]
[InlineData("application/*", false)]
[InlineData("*/*", false)]
public void CanRead_ReturnsTrueOnlyForJsonPatchContentType(string requestContentType, bool expectedCanRead)
{
// Arrange

View File

@ -56,10 +56,10 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
// Mono issue - https://github.com/aspnet/External/issues/18
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[InlineData("application/xml", true)]
[InlineData("application/*", true)]
[InlineData("*/*", true)]
[InlineData("application/*", false)]
[InlineData("*/*", false)]
[InlineData("text/xml", true)]
[InlineData("text/*", true)]
[InlineData("text/*", false)]
[InlineData("text/json", false)]
[InlineData("application/json", false)]
[InlineData("", false)]

View File

@ -41,10 +41,10 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
[Theory]
[InlineData("application/xml", true)]
[InlineData("application/*", true)]
[InlineData("*/*", true)]
[InlineData("application/*", false)]
[InlineData("*/*", false)]
[InlineData("text/xml", true)]
[InlineData("text/*", true)]
[InlineData("text/*", false)]
[InlineData("text/json", false)]
[InlineData("application/json", false)]
[InlineData("", false)]

View File

@ -170,7 +170,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
"<DummyClass xmlns=\"http://schemas.datacontract.org/2004/07/ActionResultsWebSite\">" +
"<SampleInt>2</SampleInt><SampleString>foo</SampleString></DummyClass>";
var request = new HttpRequestMessage(HttpMethod.Post, "Home/GetCustomErrorObject");
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;charset=utf-8"));
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
request.Content = new StringContent(input, Encoding.UTF8, "application/xml");
// Act
@ -218,7 +218,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal("content", await response.Content.ReadAsStringAsync());
}
[Theory]
[Fact]
public async Task ContentResult_WritesContent_SetsContentTypeAndEncoding()
{
// Arrange

View File

@ -43,10 +43,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
[Theory]
[InlineData("application/json")]
[InlineData("application/*")]
[InlineData("*/*")]
[InlineData("text/json")]
[InlineData("text/*")]
public async Task JsonInputFormatter_IsSelectedForJsonRequest(string requestContentType)
{
// Arrange

View File

@ -100,7 +100,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
{
// Arrange
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Home/ReturnUser");
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml;charset=utf-8"));
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml"));
// Act
var response = await Client.SendAsync(request);

View File

@ -21,7 +21,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
public HttpClient Client { get; }
[Theory]
[InlineData("application/xml,*/*;0.2")]
[InlineData("application/xml,*/*;q=0.2")]
[InlineData("application/xml,*/*")]
public async Task AllMediaRangeAcceptHeader_FirstFormatterInListWritesResponse(string acceptHeader)
{
@ -43,7 +43,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
[ConditionalTheory]
// Mono issue - https://github.com/aspnet/External/issues/18
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[InlineData("application/xml,*/*;0.2")]
[InlineData("application/xml,*/*;q=0.2")]
[InlineData("application/xml,*/*")]
public async Task AllMediaRangeAcceptHeader_ProducesAttributeIsHonored(string acceptHeader)
{
@ -71,9 +71,41 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
[ConditionalTheory]
// Mono issue - https://github.com/aspnet/External/issues/18
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[InlineData("application/xml,*/*;0.2")]
[InlineData("application/xml,*/*;q=0.2")]
[InlineData("application/xml,*/*")]
public async Task AllMediaRangeAcceptHeader_WithContentTypeHeader_ContentTypeIsHonored(string acceptHeader)
public async Task AllMediaRangeAcceptHeader_WithContentTypeHeader_ContentTypeIsIgnored(string acceptHeader)
{
// Arrange
var requestData =
"<RespectBrowserAcceptHeaderController.Employee xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"" +
" xmlns=\"http://schemas.datacontract.org/2004/07/FormatterWebSite.Controllers\"><Id>35</Id><Name>Jimmy" +
"</Name></RespectBrowserAcceptHeaderController.Employee>";
var expectedResponseData = @"{""Id"":35,""Name"":""Jimmy""}";
var request = RequestWithAccept("http://localhost/RespectBrowserAcceptHeader/CreateEmployee", acceptHeader);
request.Content = new StringContent(requestData, Encoding.UTF8, "application/xml");
request.Method = HttpMethod.Post;
// Act
var response = await Client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.NotNull(response.Content.Headers.ContentType);
// Site uses default output formatter (ignores Accept header) because that header contained a wildcard match.
Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString());
var responseData = await response.Content.ReadAsStringAsync();
Assert.Equal(expectedResponseData, responseData);
}
[ConditionalTheory]
// Mono issue - https://github.com/aspnet/External/issues/18
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[InlineData("application/xml,application/json;q=0.2")]
[InlineData("application/xml,application/json")]
public async Task AllMediaRangeAcceptHeader_WithExactMatch_ReturnsExpectedContent(string acceptHeader)
{
// Arrange
var requestData =

View File

@ -29,7 +29,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/Home/GetDummyClass?sampleInput=10");
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml;charset=utf-8"));
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml"));
// Act
var response = await Client.SendAsync(request);
@ -50,7 +50,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/XmlSerializer/GetDummyClass?sampleInput=10");
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml;charset=utf-8"));
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml"));
// Act
var response = await Client.SendAsync(request);
@ -72,7 +72,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/DataContractSerializer/GetPerson?name=HelloWorld");
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml;charset=utf-8"));
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml"));
// Act
var response = await Client.SendAsync(request);
@ -93,7 +93,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/XmlSerializer/GetDerivedDummyClass?sampleInput=10");
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml;charset=utf-8"));
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml"));
// Act
var response = await Client.SendAsync(request);
@ -116,7 +116,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/Home/GetDerivedDummyClass?sampleInput=10");
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml;charset=utf-8"));
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml"));
// Act
var response = await Client.SendAsync(request);
@ -137,7 +137,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/XmlSerializer/GetDictionary");
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml;charset=utf-8"));
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml"));
// Act
var response = await Client.SendAsync(request);