diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/OutputFormatterCanWriteContext.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/OutputFormatterCanWriteContext.cs
index 979dd749aa..84e34fdf46 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/OutputFormatterCanWriteContext.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/OutputFormatterCanWriteContext.cs
@@ -56,6 +56,14 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
///
public virtual StringSegment ContentType { get; set; }
+ ///
+ /// Gets or sets a value to indicate whether the content type was specified by server-side code.
+ /// This allows to
+ /// implement stricter filtering on content types that, for example, are being considered purely
+ /// of an incoming Accept header.
+ ///
+ public virtual bool ContentTypeIsServerDefined { get; set; }
+
///
/// Gets or sets the object to write to the response.
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Formatters/MediaType.cs b/src/Microsoft.AspNetCore.Mvc.Core/Formatters/MediaType.cs
index 1c7e008657..3eda392005 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Formatters/MediaType.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Formatters/MediaType.cs
@@ -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;
+ }
+
///
/// Gets the type of the .
///
+ ///
+ /// For the media type "application/json", the property gives the value "application".
+ ///
public StringSegment Type { get; }
///
@@ -171,13 +206,52 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
///
/// Gets the subtype of the .
///
+ ///
+ /// For the media type "application/vnd.example+json", the property gives the value
+ /// "vnd.example+json".
+ ///
public StringSegment SubType { get; private set; }
+ ///
+ /// Gets the subtype of the , excluding any structured syntax suffix.
+ ///
+ ///
+ /// For the media type "application/vnd.example+json", the property gives the value
+ /// "vnd.example".
+ ///
+ public StringSegment SubTypeWithoutSuffix { get; private set; }
+
+ ///
+ /// Gets the structured syntax suffix of the if it has one.
+ ///
+ ///
+ /// For the media type "application/vnd.example+json", the property gives the value
+ /// "json".
+ ///
+ public StringSegment SubTypeSuffix { get; private set; }
+
///
/// Gets whether this matches all subtypes.
///
+ ///
+ /// For the media type "application/*", this property is true.
+ ///
+ ///
+ /// For the media type "application/json", this property is false.
+ ///
public bool MatchesAllSubTypes => SubType.Equals("*", StringComparison.OrdinalIgnoreCase);
+ ///
+ /// Gets whether this matches all subtypes, ignoring any structured syntax suffix.
+ ///
+ ///
+ /// For the media type "application/*+json", this property is true.
+ ///
+ ///
+ /// For the media type "application/vnd.example+json", this property is false.
+ ///
+ public bool MatchesAllSubTypesWithoutSuffix => SubTypeWithoutSuffix.Equals("*", StringComparison.OrdinalIgnoreCase);
+
///
/// Gets the of the if it has one.
///
@@ -188,6 +262,22 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
///
public StringSegment Charset => GetParameter("charset");
+ ///
+ /// Determines whether the current contains a wildcard.
+ ///
+ ///
+ /// true if this contains a wildcard; otherwise false.
+ ///
+ public bool HasWildcard
+ {
+ get
+ {
+ return MatchesAllTypes ||
+ MatchesAllSubTypesWithoutSuffix ||
+ GetParameter("*").Equals("*", StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
///
/// Determines whether the current is a subset of the
/// .
@@ -198,8 +288,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
///
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;
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Formatters/OutputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Formatters/OutputFormatter.cs
index 28b0007641..7eb026e584 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Formatters/OutputFormatter.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Formatters/OutputFormatter.cs
@@ -49,24 +49,35 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
{
return null;
}
+
+ List 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 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();
+ }
+
+ 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;
}
///
@@ -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;
+ }
}
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectResultExecutor.cs
index ff06f6d483..fd7fff6aab 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectResultExecutor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectResultExecutor.cs
@@ -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,
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs
index 6c89fc036d..202591a1b6 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs
@@ -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));
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MediaTypeHeaderValues.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MediaTypeHeaderValues.cs
index 0b45cff678..d9fb986507 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MediaTypeHeaderValues.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MediaTypeHeaderValues.cs
@@ -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();
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs
index f924fd5c59..d588f9a79d 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs
@@ -59,16 +59,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal
{
options.OutputFormatters.Add(new JsonOutputFormatter(_jsonSerializerSettings, _charPool));
- var jsonInputLogger = _loggerFactory.CreateLogger();
- 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();
+ options.InputFormatters.Add(new JsonPatchInputFormatter(
+ jsonInputPatchLogger,
_jsonSerializerSettings,
_charPool,
_objectPoolProvider));
- var jsonInputPatchLogger = _loggerFactory.CreateLogger();
- options.InputFormatters.Add(new JsonPatchInputFormatter(
- jsonInputPatchLogger,
+ var jsonInputLogger = _loggerFactory.CreateLogger();
+ options.InputFormatters.Add(new JsonInputFormatter(
+ jsonInputLogger,
_jsonSerializerSettings,
_charPool,
_objectPoolProvider));
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs
index aed8f35194..e146e7cce1 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs
@@ -70,6 +70,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJson);
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextJson);
+ SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax);
}
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonOutputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonOutputFormatter.cs
index 98bbe2e0af..8ecdebb2e7 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonOutputFormatter.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonOutputFormatter.cs
@@ -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);
}
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MediaTypeHeaderValues.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MediaTypeHeaderValues.cs
index a71c4e5fd6..14f2a99ce4 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MediaTypeHeaderValues.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MediaTypeHeaderValues.cs
@@ -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();
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs
index 73e6ba165b..6761a12ebc 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs
@@ -35,6 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationXml);
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextXml);
+ SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyXmlSyntax);
_serializerSettings = new DataContractSerializerSettings();
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerOutputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerOutputFormatter.cs
index 17f1ea667f..8275092f83 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerOutputFormatter.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerOutputFormatter.cs
@@ -48,6 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationXml);
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextXml);
+ SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyXmlSyntax);
WriterSettings = writerSettings;
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs
index 92e1eee58b..c8b7934724 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs
@@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationXml);
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextXml);
+ SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyXmlSyntax);
WrapperProviderFactories = new List();
WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerOutputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerOutputFormatter.cs
index 5adfc6be44..018eddda32 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerOutputFormatter.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerOutputFormatter.cs
@@ -47,6 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationXml);
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextXml);
+ SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyXmlSyntax);
WriterSettings = writerSettings;
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/MediaTypeTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/MediaTypeTest.cs
index 95fa6c6a8d..1b6524085c 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/MediaTypeTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/MediaTypeTest.cs
@@ -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 MediaTypesWithSuffixes
+ {
+ get
+ {
+ return new List
+ {
+ // 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 MediaTypesWithParameters
{
get
{
return new TheoryData
{
- "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")]
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/OutputFormatterTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/OutputFormatterTests.cs
index eeb6f3ea6e..3c2e9b80a9 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/OutputFormatterTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/OutputFormatterTests.cs
@@ -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
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs
index 8f055dc501..e7482d404f 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs
@@ -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
+ {
+ 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
+ {
+ 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
+ {
+ 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);
+ }
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ProducesAttributeTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ProducesAttributeTests.cs
index 44dc11e471..3488442798 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ProducesAttributeTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ProducesAttributeTests.cs
@@ -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
diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs
index 8e5530831e..75dd819910 100644
--- a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs
@@ -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)]
diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonOutputFormatterTests.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonOutputFormatterTests.cs
index b71ed5a3a2..53e52f677a 100644
--- a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonOutputFormatterTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonOutputFormatterTests.cs
@@ -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.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,
diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs
index 4168428d89..9c99d85692 100644
--- a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs
@@ -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)]
diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerOutputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerOutputFormatterTest.cs
index f83056d415..acabb49213 100644
--- a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerOutputFormatterTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerOutputFormatterTest.cs
@@ -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