Add support for media type suffixes (#5273, #6032)

This commit is contained in:
Steve Sanderson 2017-03-31 10:20:43 +01:00
parent c47825944d
commit 4f351bd37c
31 changed files with 821 additions and 59 deletions

View File

@ -56,6 +56,14 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// </remarks>
public virtual StringSegment ContentType { get; set; }
/// <summary>
/// Gets or sets a value to indicate whether the content type was specified by server-side code.
/// This allows <see cref="IOutputFormatter.CanWriteResult(OutputFormatterCanWriteContext)"/> to
/// implement stricter filtering on content types that, for example, are being considered purely
/// of an incoming Accept header.
/// </summary>
public virtual bool ContentTypeIsServerDefined { get; set; }
/// <summary>
/// Gets or sets the object to write to the response.
/// </summary>

View File

@ -76,6 +76,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
{
Type = new StringSegment();
SubType = new StringSegment();
SubTypeWithoutSuffix = new StringSegment();
SubTypeSuffix = new StringSegment();
return;
}
else
@ -88,11 +90,24 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
if (subTypeLength == 0)
{
SubType = new StringSegment();
SubTypeWithoutSuffix = new StringSegment();
SubTypeSuffix = new StringSegment();
return;
}
else
{
SubType = subType;
if (TryGetSuffixLength(subType, out var subtypeSuffixLength))
{
SubTypeWithoutSuffix = subType.Subsegment(0, subType.Length - subtypeSuffixLength - 1);
SubTypeSuffix = subType.Subsegment(subType.Length - subtypeSuffixLength, subtypeSuffixLength);
}
else
{
SubTypeWithoutSuffix = SubType;
SubTypeSuffix = new StringSegment();
}
}
_parameterParser = new MediaTypeParameterParser(mediaType, offset + typeLength + subTypeLength, length);
@ -158,9 +173,29 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
return current - offset;
}
private static bool TryGetSuffixLength(StringSegment subType, out int suffixLength)
{
// Find the last instance of '+', if there is one
var startPos = subType.Offset + subType.Length - 1;
for (var currentPos = startPos; currentPos >= subType.Offset; currentPos--)
{
if (subType.Buffer[currentPos] == '+')
{
suffixLength = startPos - currentPos;
return true;
}
}
suffixLength = 0;
return false;
}
/// <summary>
/// Gets the type of the <see cref="MediaType"/>.
/// </summary>
/// <example>
/// For the media type <c>"application/json"</c>, the property gives the value <c>"application"</c>.
/// </example>
public StringSegment Type { get; }
/// <summary>
@ -171,13 +206,52 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// <summary>
/// Gets the subtype of the <see cref="MediaType"/>.
/// </summary>
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, the property gives the value
/// <c>"vnd.example+json"</c>.
/// </example>
public StringSegment SubType { get; private set; }
/// <summary>
/// Gets the subtype of the <see cref="MediaType"/>, excluding any structured syntax suffix.
/// </summary>
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, the property gives the value
/// <c>"vnd.example"</c>.
/// </example>
public StringSegment SubTypeWithoutSuffix { get; private set; }
/// <summary>
/// Gets the structured syntax suffix of the <see cref="MediaType"/> if it has one.
/// </summary>
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, the property gives the value
/// <c>"json"</c>.
/// </example>
public StringSegment SubTypeSuffix { get; private set; }
/// <summary>
/// Gets whether this <see cref="MediaType"/> matches all subtypes.
/// </summary>
/// <example>
/// For the media type <c>"application/*"</c>, this property is <c>true</c>.
/// </example>
/// <example>
/// For the media type <c>"application/json"</c>, this property is <c>false</c>.
/// </example>
public bool MatchesAllSubTypes => SubType.Equals("*", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Gets whether this <see cref="MediaType"/> matches all subtypes, ignoring any structured syntax suffix.
/// </summary>
/// <example>
/// For the media type <c>"application/*+json"</c>, this property is <c>true</c>.
/// </example>
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, this property is <c>false</c>.
/// </example>
public bool MatchesAllSubTypesWithoutSuffix => SubTypeWithoutSuffix.Equals("*", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Gets the <see cref="System.Text.Encoding"/> of the <see cref="MediaType"/> if it has one.
/// </summary>
@ -188,6 +262,22 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// </summary>
public StringSegment Charset => GetParameter("charset");
/// <summary>
/// Determines whether the current <see cref="MediaType"/> contains a wildcard.
/// </summary>
/// <returns>
/// <c>true</c> if this <see cref="MediaType"/> contains a wildcard; otherwise <c>false</c>.
/// </returns>
public bool HasWildcard
{
get
{
return MatchesAllTypes ||
MatchesAllSubTypesWithoutSuffix ||
GetParameter("*").Equals("*", StringComparison.OrdinalIgnoreCase);
}
}
/// <summary>
/// Determines whether the current <see cref="MediaType"/> is a subset of the <paramref name="set"/>
/// <see cref="MediaType"/>.
@ -198,8 +288,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// </returns>
public bool IsSubsetOf(MediaType set)
{
return (set.MatchesAllTypes || set.Type.Equals(Type, StringComparison.OrdinalIgnoreCase)) &&
(set.MatchesAllSubTypes || set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase)) &&
return MatchesType(set) &&
MatchesSubtype(set) &&
ContainsAllParameters(set._parameterParser);
}
@ -372,6 +462,53 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
return $"{mediaType.Value}; charset={encoding.WebName}";
}
private bool MatchesType(MediaType set)
{
return set.MatchesAllTypes ||
set.Type.Equals(Type, StringComparison.OrdinalIgnoreCase);
}
private bool MatchesSubtype(MediaType set)
{
if (set.MatchesAllSubTypes)
{
return true;
}
if (set.SubTypeSuffix.HasValue)
{
if (SubTypeSuffix.HasValue)
{
// Both the set and the media type being checked have suffixes, so both parts must match.
return MatchesSubtypeWithoutSuffix(set) && MatchesSubtypeSuffix(set);
}
else
{
// The set has a suffix, but the media type being checked doesn't. We never consider this to match.
return false;
}
}
else
{
// The set has no suffix, so we're just looking for an exact match (which means that if 'this'
// has a suffix, it won't match).
return set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase);
}
}
private bool MatchesSubtypeWithoutSuffix(MediaType set)
{
return set.MatchesAllSubTypesWithoutSuffix ||
set.SubTypeWithoutSuffix.Equals(SubTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase);
}
private bool MatchesSubtypeSuffix(MediaType set)
{
// We don't have support for wildcards on suffixes alone (e.g., "application/entity+*")
// because there's no clear use case for it.
return set.SubTypeSuffix.Equals(SubTypeSuffix, StringComparison.OrdinalIgnoreCase);
}
private bool ContainsAllParameters(MediaTypeParameterParser setParameters)
{
var parameterFound = true;
@ -385,6 +522,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
break;
}
if (setParameter.HasName("*"))
{
// A parameter named "*" has no effect on media type matching, as it is only used as an indication
// that the entire media type string should be treated as a wildcard.
continue;
}
// Copy the parser as we need to iterate multiple times over it.
// We can do this because it's a struct
var subSetParameters = _parameterParser;
@ -452,8 +596,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
if (nameLength == 0 || OffsetIsOutOfRange(current, input.Length) || input[current] != '=')
{
parsedValue = default(MediaTypeParameter);
return 0;
if (current == input.Length && name.Equals("*", StringComparison.OrdinalIgnoreCase))
{
// As a special case, we allow a trailing ";*" to indicate a wildcard
// string allowing any other parameters. It's the same as ";*=*".
var asterisk = new StringSegment("*");
parsedValue = new MediaTypeParameter(asterisk, asterisk);
return current - startIndex;
}
else
{
parsedValue = default(MediaTypeParameter);
return 0;
}
}
StringSegment value;

View File

@ -49,24 +49,35 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
{
return null;
}
List<string> mediaTypes = null;
if (contentType == null)
var parsedContentType = contentType != null ? new MediaType(contentType) : default(MediaType);
foreach (var mediaType in SupportedMediaTypes)
{
// If contentType is null, then any type we support is valid.
return SupportedMediaTypes;
}
else
{
List<string> mediaTypes = null;
var parsedContentType = new MediaType(contentType);
// 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)
var parsedMediaType = new MediaType(mediaType);
if (parsedMediaType.HasWildcard)
{
var parsedMediaType = new MediaType(mediaType);
if (parsedMediaType.IsSubsetOf(parsedContentType))
// For supported media types that are wildcard patterns, confirm that the requested
// media type satisfies the wildcard pattern (e.g., if "text/entity+json;v=2" requested
// and formatter supports "text/*+json").
// Treat contentType like it came from a [Produces] attribute.
if (contentType != null && parsedContentType.IsSubsetOf(parsedMediaType))
{
if (mediaTypes == null)
{
mediaTypes = new List<string>();
}
mediaTypes.Add(contentType);
}
}
else
{
// 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.
if (contentType == null || parsedMediaType.IsSubsetOf(parsedContentType))
{
if (mediaTypes == null)
{
@ -76,9 +87,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
mediaTypes.Add(mediaType);
}
}
return mediaTypes;
}
return mediaTypes;
}
/// <inheritdoc />
@ -112,17 +123,36 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
else
{
// 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.
// Confirm this formatter
var parsedContentType = new MediaType(context.ContentType);
for (var i = 0; i < SupportedMediaTypes.Count; i++)
{
var supportedMediaType = new MediaType(SupportedMediaTypes[i]);
if (supportedMediaType.IsSubsetOf(parsedContentType))
if (supportedMediaType.HasWildcard)
{
context.ContentType = new StringSegment(SupportedMediaTypes[i]);
return true;
// For supported media types that are wildcard patterns, confirm that the requested
// media type satisfies the wildcard pattern (e.g., if "text/entity+json;v=2" requested
// and formatter supports "text/*+json").
// We only do this when comparing against server-defined content types (e.g., those
// from [Produces] or Response.ContentType), otherwise we'd potentially be reflecting
// back arbitrary Accept header values.
if (context.ContentTypeIsServerDefined
&& parsedContentType.IsSubsetOf(supportedMediaType))
{
return true;
}
}
else
{
// For supported media types that are not wildcard patterns, confirm that 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.
if (supportedMediaType.IsSubsetOf(parsedContentType))
{
context.ContentType = new StringSegment(SupportedMediaTypes[i]);
return true;
}
}
}
}

View File

@ -302,6 +302,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
foreach (var formatter in formatters)
{
formatterContext.ContentType = new StringSegment();
formatterContext.ContentTypeIsServerDefined = false;
if (formatter.CanWriteResult(formatterContext))
{
return formatter;
@ -349,6 +350,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
var mediaType = sortedAcceptHeaders[i];
formatterContext.ContentType = mediaType.MediaType;
formatterContext.ContentTypeIsServerDefined = false;
for (var j = 0; j < formatters.Count; j++)
{
var formatter = formatters[j];
@ -401,6 +403,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
foreach (var contentType in acceptableContentTypes)
{
formatterContext.ContentType = new StringSegment(contentType);
formatterContext.ContentTypeIsServerDefined = true;
if (formatter.CanWriteResult(formatterContext))
{
return formatter;
@ -446,6 +449,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
var formatter = formatters[k];
formatterContext.ContentType = new StringSegment(possibleOutputContentTypes[j]);
formatterContext.ContentTypeIsServerDefined = true;
if (formatter.CanWriteResult(formatterContext))
{
return formatter;
@ -469,8 +473,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
var contentType = contentTypes[i];
var parsedContentType = new MediaType(contentType);
if (parsedContentType.MatchesAllTypes ||
parsedContentType.MatchesAllSubTypes)
if (parsedContentType.HasWildcard)
{
var message = Resources.FormatObjectResult_MatchAllContentType(
contentType,

View File

@ -126,8 +126,7 @@ namespace Microsoft.AspNetCore.Mvc
foreach (var arg in completeArgs)
{
var contentType = new MediaType(arg);
if (contentType.MatchesAllTypes ||
contentType.MatchesAllSubTypes)
if (contentType.HasWildcard)
{
throw new InvalidOperationException(
Resources.FormatMatchAllContentTypeIsNotAllowed(arg));

View File

@ -15,5 +15,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal
public static readonly MediaTypeHeaderValue ApplicationJsonPatch
= MediaTypeHeaderValue.Parse("application/json-patch+json").CopyAsReadOnly();
public static readonly MediaTypeHeaderValue ApplicationAnyJsonSyntax
= MediaTypeHeaderValue.Parse("application/*+json").CopyAsReadOnly();
}
}

View File

@ -59,16 +59,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal
{
options.OutputFormatters.Add(new JsonOutputFormatter(_jsonSerializerSettings, _charPool));
var jsonInputLogger = _loggerFactory.CreateLogger<JsonInputFormatter>();
options.InputFormatters.Add(new JsonInputFormatter(
jsonInputLogger,
// Register JsonPatchInputFormatter before JsonInputFormatter, otherwise
// JsonInputFormatter would consume "application/json-patch+json" requests
// before JsonPatchInputFormatter gets to see them.
var jsonInputPatchLogger = _loggerFactory.CreateLogger<JsonPatchInputFormatter>();
options.InputFormatters.Add(new JsonPatchInputFormatter(
jsonInputPatchLogger,
_jsonSerializerSettings,
_charPool,
_objectPoolProvider));
var jsonInputPatchLogger = _loggerFactory.CreateLogger<JsonPatchInputFormatter>();
options.InputFormatters.Add(new JsonPatchInputFormatter(
jsonInputPatchLogger,
var jsonInputLogger = _loggerFactory.CreateLogger<JsonInputFormatter>();
options.InputFormatters.Add(new JsonInputFormatter(
jsonInputLogger,
_jsonSerializerSettings,
_charPool,
_objectPoolProvider));

View File

@ -70,6 +70,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJson);
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextJson);
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax);
}
/// <summary>

View File

@ -50,6 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SupportedEncodings.Add(Encoding.Unicode);
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJson);
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextJson);
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax);
}
/// <summary>

View File

@ -12,5 +12,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal
public static readonly MediaTypeHeaderValue TextXml
= MediaTypeHeaderValue.Parse("text/xml").CopyAsReadOnly();
public static readonly MediaTypeHeaderValue ApplicationAnyXmlSyntax
= MediaTypeHeaderValue.Parse("application/*+xml").CopyAsReadOnly();
}
}

View File

@ -35,6 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationXml);
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextXml);
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyXmlSyntax);
_serializerSettings = new DataContractSerializerSettings();

View File

@ -48,6 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationXml);
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextXml);
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyXmlSyntax);
WriterSettings = writerSettings;

View File

@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationXml);
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextXml);
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyXmlSyntax);
WrapperProviderFactories = new List<IWrapperProviderFactory>();
WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());

View File

@ -47,6 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationXml);
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextXml);
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyXmlSyntax);
WriterSettings = writerSettings;

View File

@ -2,6 +2,7 @@
// 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.Text;
using Microsoft.Extensions.Primitives;
using Xunit;
@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
[InlineData("application/json")]
[InlineData("application /json")]
[InlineData(" application / json ")]
public void Constructor_CanParseParameterlessMediaTypes(string mediaType)
public void Constructor_CanParseParameterlessSuffixlessMediaTypes(string mediaType)
{
// Arrange & Act
var result = new MediaType(mediaType, 0, mediaType.Length);
@ -24,23 +25,54 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Assert.Equal(new StringSegment("json"), result.SubType);
}
public static IEnumerable<string[]> MediaTypesWithSuffixes
{
get
{
return new List<string[]>
{
// See https://tools.ietf.org/html/rfc6838#section-4.2 for allowed names spec
new[] { "application/json", "json", null },
new[] { "application/json+", "json", "" },
new[] { "application/+json", "", "json" },
new[] { "application/entitytype+json", "entitytype", "json" },
new[] { " application / vnd.com-pany.some+entity!.v2+js.#$&^_n ; q=\"0.3+1\"", "vnd.com-pany.some+entity!.v2", "js.#$&^_n" },
};
}
}
[Theory]
[MemberData(nameof (MediaTypesWithSuffixes))]
public void Constructor_CanParseSuffixedMediaTypes(
string mediaType,
string expectedSubTypeWithoutSuffix,
string expectedSubtypeSuffix)
{
// Arrange & Act
var result = new MediaType(mediaType);
// Assert
Assert.Equal(new StringSegment(expectedSubTypeWithoutSuffix), result.SubTypeWithoutSuffix);
Assert.Equal(new StringSegment(expectedSubtypeSuffix), result.SubTypeSuffix);
}
public static TheoryData<string> MediaTypesWithParameters
{
get
{
return new TheoryData<string>
{
"application/json;format=pretty;charset=utf-8;q=0.8",
"application/json;format=pretty;charset=\"utf-8\";q=0.8",
"application/json;format=pretty;charset=utf-8; q=0.8 ",
"application/json;format=pretty;charset=utf-8 ; q=0.8 ",
"application/json;format=pretty; charset=utf-8 ; q=0.8 ",
"application/json;format=pretty ; charset=utf-8 ; q=0.8 ",
"application/json; format=pretty ; charset=utf-8 ; q=0.8 ",
"application/json; format=pretty ; charset=utf-8 ; q= 0.8 ",
"application/json; format=pretty ; charset=utf-8 ; q = 0.8 ",
" application / json; format = pretty ; charset = utf-8 ; q = 0.8 ",
" application / json; format = \"pretty\" ; charset = \"utf-8\" ; q = \"0.8\" ",
"application/json+bson;format=pretty;charset=utf-8;q=0.8",
"application/json+bson;format=pretty;charset=\"utf-8\";q=0.8",
"application/json+bson;format=pretty;charset=utf-8; q=0.8 ",
"application/json+bson;format=pretty;charset=utf-8 ; q=0.8 ",
"application/json+bson;format=pretty; charset=utf-8 ; q=0.8 ",
"application/json+bson;format=pretty ; charset=utf-8 ; q=0.8 ",
"application/json+bson; format=pretty ; charset=utf-8 ; q=0.8 ",
"application/json+bson; format=pretty ; charset=utf-8 ; q= 0.8 ",
"application/json+bson; format=pretty ; charset=utf-8 ; q = 0.8 ",
" application / json+bson; format = pretty ; charset = utf-8 ; q = 0.8 ",
" application / json+bson; format = \"pretty\" ; charset = \"utf-8\" ; q = \"0.8\" ",
};
}
}
@ -54,7 +86,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
// Assert
Assert.Equal(new StringSegment("application"), result.Type);
Assert.Equal(new StringSegment("json"), result.SubType);
Assert.Equal(new StringSegment("json+bson"), result.SubType);
Assert.Equal(new StringSegment("json"), result.SubTypeWithoutSuffix);
Assert.Equal(new StringSegment("bson"), result.SubTypeSuffix);
Assert.Equal(new StringSegment("pretty"), result.GetParameter("format"));
Assert.Equal(new StringSegment("0.8"), result.GetParameter("q"));
Assert.Equal(new StringSegment("utf-8"), result.GetParameter("charset"));
@ -171,6 +205,15 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
[InlineData("application/json", "application/json;format=indent;charset=utf-8")]
[InlineData("application/json;format=indent;charset=utf-8", "application/json;format=indent;charset=utf-8")]
[InlineData("application/json;charset=utf-8;format=indent", "application/json;format=indent;charset=utf-8")]
[InlineData("application/*", "application/json")]
[InlineData("application/*", "application/entitytype+json;v=2")]
[InlineData("application/*;v=2", "application/entitytype+json;v=2")]
[InlineData("application/json;*", "application/json;v=2")]
[InlineData("application/json;v=2;*", "application/json;v=2;charset=utf-8")]
[InlineData("*/*", "application/json")]
[InlineData("application/entity+json", "application/entity+json")]
[InlineData("application/*+json", "application/entity+json")]
[InlineData("application/*", "application/entity+json")]
public void IsSubsetOf_ReturnsTrueWhenExpected(string set, string subset)
{
// Arrange
@ -188,6 +231,17 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
[InlineData("application/json;charset=utf-8", "application/json")]
[InlineData("application/json;format=indent;charset=utf-8", "application/json")]
[InlineData("application/json;format=indent;charset=utf-8", "application/json;charset=utf-8")]
[InlineData("application/*", "text/json")]
[InlineData("application/*;v=2", "application/json")]
[InlineData("application/*;v=2", "application/json;v=1")]
[InlineData("application/json;v=2;*", "application/json;v=1")]
[InlineData("application/entity+json", "application/entity+txt")]
[InlineData("application/entity+json", "application/entity.v2+json")]
[InlineData("application/*+json", "application/entity+txt")]
[InlineData("application/entity+*", "application/entity.v2+json")]
[InlineData("application/*+*", "application/json")]
[InlineData("application/entity+*", "application/entity+json")] // We don't allow suffixes to be wildcards
[InlineData("application/*+*", "application/entity+json")] // We don't allow suffixes to be wildcards
public void IsSubsetOf_ReturnsFalseWhenExpected(string set, string subset)
{
// Arrange
@ -257,6 +311,48 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Assert.False(result);
}
[Theory]
[InlineData("*/*", true)]
[InlineData("text/*", true)]
[InlineData("text/*+suffix", true)]
[InlineData("text/*+", true)]
[InlineData("text/*+*", true)]
[InlineData("text/json+suffix", false)]
[InlineData("*/json+*", false)]
public void MatchesAllSubTypesWithoutSuffix_ReturnsExpectedResult(string value, bool expectedReturnValue)
{
// Arrange
var mediaType = new MediaType(value);
// Act
var result = mediaType.MatchesAllSubTypesWithoutSuffix;
// Assert
Assert.Equal(expectedReturnValue, result);
}
[Theory]
[InlineData("*/*", true)]
[InlineData("text/*", true)]
[InlineData("text/entity+*", false)] // We don't support wildcards on suffixes
[InlineData("text/*+json", true)]
[InlineData("text/entity+json;*", true)]
[InlineData("text/entity+json;v=3;*", true)]
[InlineData("text/entity+json;v=3;q=0.8", false)]
[InlineData("text/json", false)]
[InlineData("text/json;param=*", false)] // * is the literal value of the param
public void HasWildcard_ReturnsTrueWhenExpected(string value, bool expectedReturnValue)
{
// Arrange
var mediaType = new MediaType(value);
// Act
var result = mediaType.HasWildcard;
// Assert
Assert.Equal(expectedReturnValue, result);
}
[Theory]
[MemberData(nameof(MediaTypesWithParameters))]
[InlineData("application/json;format=pretty;q=0.9;charset=utf-8;q=0.8")]

View File

@ -75,13 +75,47 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Assert.False(result);
}
[Theory]
[InlineData(true, true)]
[InlineData(false, false)]
public void CanWriteResult_MatchesWildcardsOnlyWhenContentTypeProvidedByServer(
bool contentTypeProvidedByServer, bool shouldMatchWildcards)
{
// Arrange
var formatter = new TypeSpecificFormatter();
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/*+xml"));
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/*+json"));
formatter.SupportedTypes.Add(typeof(string));
var requestedContentType = "application/vnd.test.entity+json;v=2";
var context = new OutputFormatterWriteContext(
new DefaultHttpContext(),
new TestHttpResponseStreamWriterFactory().CreateWriter,
typeof(string),
"Hello, world!")
{
ContentType = new StringSegment(requestedContentType),
ContentTypeIsServerDefined = contentTypeProvidedByServer,
};
// Act
var result = formatter.CanWriteResult(context);
// Assert
Assert.Equal(shouldMatchWildcards, result);
Assert.Equal(requestedContentType, context.ContentType.ToString());
}
[Fact]
public void GetSupportedContentTypes_ReturnsAllContentTypes_WithContentTypeNull()
public void GetSupportedContentTypes_ReturnsAllNonWildcardContentTypes_WithContentTypeNull()
{
// Arrange
var formatter = new TestOutputFormatter();
formatter.SupportedMediaTypes.Clear();
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("*/*"));
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/*"));
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/plain;*"));
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/xml"));
// Act
@ -96,7 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
[Fact]
public void GetSupportedContentTypes_ReturnsMatchingContentTypes_WithContentType()
public void GetSupportedContentTypes_ReturnsMoreSpecificMatchingContentTypes_WithContentType()
{
// Arrange
var formatter = new TestOutputFormatter();
@ -116,18 +150,39 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
[Fact]
public void GetSupportedContentTypes_ReturnsMatchingContentTypes_NoMatches()
public void GetSupportedContentTypes_ReturnsMatchingWildcardContentTypes_WithContentType()
{
// Arrange
var formatter = new TestOutputFormatter();
formatter.SupportedMediaTypes.Clear();
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/*+json"));
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
// Act
var contentTypes = formatter.GetSupportedContentTypes(
"application/xml",
"application/vnd.test+json;v=2",
typeof(int));
// Assert
var contentType = Assert.Single(contentTypes);
Assert.Equal("application/vnd.test+json;v=2", contentType.ToString());
}
[Fact]
public void GetSupportedContentTypes_ReturnsMatchingContentTypes_NoMatches()
{
// Arrange
var formatter = new TestOutputFormatter();
formatter.SupportedMediaTypes.Clear();
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/*+xml"));
formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
// Act
var contentTypes = formatter.GetSupportedContentTypes(
"application/vnd.test+bson",
typeof(int));
// Assert

View File

@ -342,6 +342,100 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Null(formatter);
}
[Fact]
public void SelectFormatter_WithAcceptHeaderOnly_SetsContentTypeIsServerDefinedToFalse()
{
// Arrange
var executor = CreateExecutor();
var formatters = new List<IOutputFormatter>
{
new ServerContentTypeOnlyFormatter()
};
var context = new OutputFormatterWriteContext(
new DefaultHttpContext(),
new TestHttpResponseStreamWriterFactory().CreateWriter,
objectType: null,
@object: null);
context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom";
// Act
var formatter = executor.SelectFormatter(
context,
new MediaTypeCollection { },
formatters);
// Assert
Assert.Null(formatter);
}
[Fact]
public void SelectFormatter_WithAcceptHeaderAndContentTypes_SetsContentTypeIsServerDefinedWhenExpected()
{
// Arrange
var executor = CreateExecutor();
var formatters = new List<IOutputFormatter>
{
new ServerContentTypeOnlyFormatter()
};
var context = new OutputFormatterWriteContext(
new DefaultHttpContext(),
new TestHttpResponseStreamWriterFactory().CreateWriter,
objectType: null,
@object: null);
context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom, text/custom2";
var serverDefinedContentTypes = new MediaTypeCollection();
serverDefinedContentTypes.Add("text/other");
serverDefinedContentTypes.Add("text/custom2");
// Act
var formatter = executor.SelectFormatter(
context,
serverDefinedContentTypes,
formatters);
// Assert
Assert.Same(formatters[0], formatter);
Assert.Equal(new StringSegment("text/custom2"), context.ContentType);
}
[Fact]
public void SelectFormatter_WithContentTypesOnly_SetsContentTypeIsServerDefinedToTrue()
{
// Arrange
var executor = CreateExecutor();
var formatters = new List<IOutputFormatter>
{
new ServerContentTypeOnlyFormatter()
};
var context = new OutputFormatterWriteContext(
new DefaultHttpContext(),
new TestHttpResponseStreamWriterFactory().CreateWriter,
objectType: null,
@object: null);
var serverDefinedContentTypes = new MediaTypeCollection();
serverDefinedContentTypes.Add("text/custom");
// Act
var formatter = executor.SelectFormatter(
context,
serverDefinedContentTypes,
formatters);
// Assert
Assert.Same(formatters[0], formatter);
Assert.Equal(new StringSegment("text/custom"), context.ContentType);
}
[Fact]
public async Task ExecuteAsync_NoFormatterFound_Returns406()
{
@ -419,6 +513,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
[InlineData(new[] { "*/*" }, "*/*")]
[InlineData(new[] { "application/xml", "*/*", "application/json" }, "*/*")]
[InlineData(new[] { "*/*", "application/json" }, "*/*")]
[InlineData(new[] { "application/json", "application/*+json" }, "application/*+json")]
[InlineData(new[] { "application/entiy+json;*", "application/json" }, "application/entiy+json;*")]
public async Task ExecuteAsync_MatchAllContentType_Throws(string[] contentTypes, string invalidContentType)
{
// Arrange
@ -656,5 +752,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return SelectedOutputFormatter;
}
}
private class ServerContentTypeOnlyFormatter : OutputFormatter
{
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
// This test formatter matches if and only if the content type is specified
// as "server defined". This lets tests identify what value the ObjectResultExecutor
// passed for that flag.
return context.ContentTypeIsServerDefined;
}
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
{
return Task.FromResult(0);
}
}
}
}

View File

@ -116,6 +116,8 @@ namespace Microsoft.AspNetCore.Mvc.Test
[InlineData("*/*", "*/*")]
[InlineData("application/xml, */*, application/json", "*/*")]
[InlineData("*/*, application/json", "*/*")]
[InlineData("application/*+json", "application/*+json")]
[InlineData("application/json;v=1;*", "application/json;v=1;*")]
public void ProducesAttribute_InvalidContentType_Throws(string content, string invalidContentType)
{
// Act

View File

@ -33,6 +33,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
[InlineData("text/*", false)]
[InlineData("text/xml", false)]
[InlineData("application/xml", false)]
[InlineData("application/some.entity+json", true)]
[InlineData("application/some.entity+json;v=2", true)]
[InlineData("application/some.entity+xml", false)]
[InlineData("application/some.entity+*", false)]
[InlineData("text/some.entity+json", false)]
[InlineData("", false)]
[InlineData(null, false)]
[InlineData("invalid", false)]

View File

@ -401,6 +401,49 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Assert.Equal(expectedOutput, content);
}
[Theory]
[InlineData("application/json", false, "application/json")]
[InlineData("application/json", true, "application/json")]
[InlineData("application/xml", false, null)]
[InlineData("application/xml", true, null)]
[InlineData("application/*", false, "application/json")]
[InlineData("text/*", false, "text/json")]
[InlineData("custom/*", false, null)]
[InlineData("application/json;v=2", false, null)]
[InlineData("application/json;v=2", true, null)]
[InlineData("application/some.entity+json", false, null)]
[InlineData("application/some.entity+json", true, "application/some.entity+json")]
[InlineData("application/some.entity+json;v=2", true, "application/some.entity+json;v=2")]
[InlineData("application/some.entity+xml", true, null)]
public void CanWriteResult_ReturnsExpectedValueForMediaType(
string mediaType,
bool isServerDefined,
string expectedResult)
{
// Arrange
var formatter = new JsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared);
var body = new MemoryStream();
var actionContext = GetActionContext(MediaTypeHeaderValue.Parse(mediaType), body);
var outputFormatterContext = new OutputFormatterWriteContext(
actionContext.HttpContext,
new TestHttpResponseStreamWriterFactory().CreateWriter,
typeof(string),
new object())
{
ContentType = new StringSegment(mediaType),
ContentTypeIsServerDefined = isServerDefined,
};
// Act
var actualCanWriteValue = formatter.CanWriteResult(outputFormatterContext);
// Assert
var expectedContentType = expectedResult ?? mediaType;
Assert.Equal(expectedResult != null, actualCanWriteValue);
Assert.Equal(new StringSegment(expectedContentType), outputFormatterContext.ContentType);
}
private static Encoding CreateOrGetSupportedEncoding(
JsonOutputFormatter formatter,
string encodingAsString,

View File

@ -58,6 +58,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
[InlineData("text/*", false)]
[InlineData("text/json", false)]
[InlineData("application/json", false)]
[InlineData("application/some.entity+xml", true)]
[InlineData("application/some.entity+xml;v=2", true)]
[InlineData("application/some.entity+json", false)]
[InlineData("application/some.entity+*", false)]
[InlineData("text/some.entity+json", false)]
[InlineData("", false)]
[InlineData(null, false)]
[InlineData("invalid", false)]

View File

@ -377,7 +377,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
// Mono issue - https://github.com/aspnet/External/issues/18
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[MemberData(nameof(TypesForCanWriteResult))]
public void CanWriteResult_ReturnsExpectedOutput(object input, Type declaredType, bool expectedOutput)
public void CanWriteResult_ReturnsExpectedValueForObjectType(object input, Type declaredType, bool expectedOutput)
{
// Arrange
var formatter = new XmlDataContractSerializerOutputFormatter();
@ -391,6 +391,42 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
Assert.Equal(expectedOutput, result);
}
[ConditionalTheory]
// Mono issue - https://github.com/aspnet/External/issues/18
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[InlineData("application/xml", false, "application/xml")]
[InlineData("application/xml", true, "application/xml")]
[InlineData("application/other", false, null)]
[InlineData("application/other", true, null)]
[InlineData("application/*", false, "application/xml")]
[InlineData("text/*", false, "text/xml")]
[InlineData("custom/*", false, null)]
[InlineData("application/xml;v=2", false, null)]
[InlineData("application/xml;v=2", true, null)]
[InlineData("application/some.entity+xml", false, null)]
[InlineData("application/some.entity+xml", true, "application/some.entity+xml")]
[InlineData("application/some.entity+xml;v=2", true, "application/some.entity+xml;v=2")]
[InlineData("application/some.entity+other", true, null)]
public void CanWriteResult_ReturnsExpectedValueForMediaType(
string mediaType,
bool isServerDefined,
string expectedResult)
{
// Arrange
var formatter = new XmlDataContractSerializerOutputFormatter();
var outputFormatterContext = GetOutputFormatterContext(new object(), typeof(object));
outputFormatterContext.ContentType = new StringSegment(mediaType);
outputFormatterContext.ContentTypeIsServerDefined = isServerDefined;
// Act
var actualCanWriteValue = formatter.CanWriteResult(outputFormatterContext);
// Assert
var expectedContentType = expectedResult ?? mediaType;
Assert.Equal(expectedResult != null, actualCanWriteValue);
Assert.Equal(new StringSegment(expectedContentType), outputFormatterContext.ContentType);
}
public static IEnumerable<object[]> TypesForGetSupportedContentTypes
{
get

View File

@ -48,6 +48,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
[InlineData("text/*", false)]
[InlineData("text/json", false)]
[InlineData("application/json", false)]
[InlineData("application/some.entity+xml", true)]
[InlineData("application/some.entity+xml;v=2", true)]
[InlineData("application/some.entity+json", false)]
[InlineData("application/some.entity+*", false)]
[InlineData("text/some.entity+json", false)]
[InlineData("", false)]
[InlineData("invalid", false)]
[InlineData(null, false)]

View File

@ -295,7 +295,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
[Theory]
[MemberData(nameof(TypesForCanWriteResult))]
public void XmlSerializer_CanWriteResult(object input, Type declaredType, bool expectedOutput)
public void CanWriteResult_ReturnsExpectedValueForObjectType(object input, Type declaredType, bool expectedOutput)
{
// Arrange
var formatter = new XmlSerializerOutputFormatter();
@ -309,6 +309,40 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
Assert.Equal(expectedOutput, result);
}
[Theory]
[InlineData("application/xml", false, "application/xml")]
[InlineData("application/xml", true, "application/xml")]
[InlineData("application/other", false, null)]
[InlineData("application/other", true, null)]
[InlineData("application/*", false, "application/xml")]
[InlineData("text/*", false, "text/xml")]
[InlineData("custom/*", false, null)]
[InlineData("application/xml;v=2", false, null)]
[InlineData("application/xml;v=2", true, null)]
[InlineData("application/some.entity+xml", false, null)]
[InlineData("application/some.entity+xml", true, "application/some.entity+xml")]
[InlineData("application/some.entity+xml;v=2", true, "application/some.entity+xml;v=2")]
[InlineData("application/some.entity+other", true, null)]
public void CanWriteResult_ReturnsExpectedValueForMediaType(
string mediaType,
bool isServerDefined,
string expectedResult)
{
// Arrange
var formatter = new XmlSerializerOutputFormatter();
var outputFormatterContext = GetOutputFormatterContext(new object(), typeof (object));
outputFormatterContext.ContentType = new StringSegment(mediaType);
outputFormatterContext.ContentTypeIsServerDefined = isServerDefined;
// Act
var actualCanWriteValue = formatter.CanWriteResult(outputFormatterContext);
// Assert
var expectedContentType = expectedResult ?? mediaType;
Assert.Equal(expectedResult != null, actualCanWriteValue);
Assert.Equal(new StringSegment(expectedContentType), outputFormatterContext.ContentType);
}
[Fact]
public async Task XmlSerializerOutputFormatterDoesntFlushOutputStream()
{

View File

@ -817,6 +817,25 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(typeof(JsonOutputFormatter).FullName, textJson.FormatterType);
}
[Fact]
public async Task ApiExplorer_ResponseContentType_WildcardMatch()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/ApiExplorerResponseContentType/WildcardMatch");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
var responseType = Assert.Single(description.SupportedResponseTypes);
Assert.Equal(1, responseType.ResponseFormats.Count);
var responseFormat = responseType.ResponseFormats[0];
Assert.Equal("application/hal+json", responseFormat.MediaType);
Assert.Equal(typeof(JsonOutputFormatter).FullName, responseFormat.FormatterType);
}
[Fact]
public async Task ApiExplorer_ResponseContentType_NoMatch()
{

View File

@ -123,5 +123,47 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedString, product.SampleString);
}
[Fact]
public async Task JsonSyntaxSuffix_SelectsActionConsumingJson()
{
// Arrange
var input = "{SampleString:\"some input\"}";
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/ConsumesAttribute_MediaTypeSuffix/CreateProduct");
request.Content = new StringContent(input, Encoding.UTF8, "application/vnd.example+json");
// Act
var response = await Client.SendAsync(request);
var product = JsonConvert.DeserializeObject<Product>(await response.Content.ReadAsStringAsync());
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Read from JSON: some input", product.SampleString);
}
[ConditionalFact]
// Mono issue - https://github.com/aspnet/External/issues/18
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
public async Task XmlSyntaxSuffix_SelectsActionConsumingXml()
{
// Arrange
var input = "<Product xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" " +
"xmlns=\"http://schemas.datacontract.org/2004/07/BasicWebSite.Models\">" +
"<SampleString>some input</SampleString></Product>";
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/ConsumesAttribute_MediaTypeSuffix/CreateProduct");
request.Content = new StringContent(input, Encoding.UTF8, "application/vnd.example+xml");
// Act
var response = await Client.SendAsync(request);
var product = JsonConvert.DeserializeObject<Product>(await response.Content.ReadAsStringAsync());
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Read from XML: some input", product.SampleString);
}
}
}

View File

@ -5,10 +5,13 @@ using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using BasicWebSite.Models;
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
using Microsoft.AspNetCore.Testing.xunit;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
@ -468,5 +471,42 @@ END:VCARD
var body = await response.Content.ReadAsStringAsync();
Assert.Equal(body, "MethodWithFormatFilter");
}
[Fact]
public async Task ProducesAttribute_CustomMediaTypeWithJsonSuffix_RunsConnegAndSelectsJsonFormatter()
{
// Arrange
var expectedMediaType = MediaTypeHeaderValue.Parse("application/vnd.example.contact+json; v=2; charset=utf-8");
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/ProducesWithMediaTypeSuffixesController/ContactInfo");
request.Headers.Add("Accept", "application/vnd.example.contact+json; v=2");
// Act
var response = await Client.SendAsync(request);
// Assert
Assert.Equal(expectedMediaType, response.Content.Headers.ContentType);
var body = await response.Content.ReadAsStringAsync();
var contact = JsonConvert.DeserializeObject<Contact>(body);
Assert.Equal("Jason Ecsemelle", contact.Name);
}
[Fact]
public async Task ProducesAttribute_CustomMediaTypeWithXmlSuffix_RunsConnegAndSelectsXmlFormatter()
{
// Arrange
var expectedMediaType = MediaTypeHeaderValue.Parse("application/vnd.example.contact+xml; v=2; charset=utf-8");
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/ProducesWithMediaTypeSuffixesController/ContactInfo");
request.Headers.Add("Accept", "application/vnd.example.contact+xml; v=2");
// Act
var response = await Client.SendAsync(request);
// Assert
Assert.Equal(expectedMediaType, response.Content.Headers.ContentType);
var bodyStream = await response.Content.ReadAsStreamAsync();
var xmlDeserializer = new DataContractSerializer(typeof(Contact));
var contact = xmlDeserializer.ReadObject(bodyStream) as Contact;
Assert.Equal("Jason Ecsemelle", contact.Name);
}
}
}

View File

@ -106,8 +106,8 @@ namespace Microsoft.AspNetCore.Mvc
// Assert
Assert.Collection(options.InputFormatters,
formatter => Assert.IsType<JsonInputFormatter>(formatter),
formatter => Assert.IsType<JsonPatchInputFormatter>(formatter));
formatter => Assert.IsType<JsonPatchInputFormatter>(formatter),
formatter => Assert.IsType<JsonInputFormatter>(formatter));
}
[Fact]

View File

@ -22,7 +22,14 @@ namespace ApiExplorerWebSite
}
[HttpGet]
[Produces("application/hal+json", "text/hal+json")]
[Produces("application/hal+custom", "application/hal+json")]
public Product WildcardMatch()
{
return null;
}
[HttpGet]
[Produces("application/custom", "text/hal+bson")]
public Product NoMatch()
{
return null;

View File

@ -0,0 +1,30 @@
// 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 BasicWebSite.Models;
using Microsoft.AspNetCore.Mvc;
namespace BasicWebSite.Controllers.ActionConstraints
{
[Route("ConsumesAttribute_MediaTypeSuffix/[action]")]
public class ConsumesAttribute_MediaTypeSuffix : Controller
{
[Consumes("application/vnd.example+json")]
public Product CreateProduct([FromBody] Product_Json jsonInput)
{
// To show that we selected the correct method (and not just the
// correct input formatter), produce method-specific output.
jsonInput.SampleString = "Read from JSON: " + jsonInput.SampleString;
return jsonInput;
}
[Consumes("application/vnd.example+xml")]
public Product CreateProduct([FromBody] Product_Xml xmlInput)
{
// To show that we selected the correct method (and not just the
// correct input formatter), produce method-specific output.
xmlInput.SampleString = "Read from XML: " + xmlInput.SampleString;
return xmlInput;
}
}
}

View File

@ -0,0 +1,21 @@
// 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 BasicWebSite.Models;
using Microsoft.AspNetCore.Mvc;
namespace BasicWebSite.Controllers.ContentNegotiation
{
[Route("ProducesWithMediaTypeSuffixesController/[action]")]
public class ProducesWithMediaTypeSuffixesController : Controller
{
[Produces("application/vnd.example.contact+json; v=2", "application/vnd.example.contact+xml; v=2")]
public Contact ContactInfo()
{
return new Contact()
{
Name = "Jason Ecsemelle"
};
}
}
}