diff --git a/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs b/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs index bfda2aac81..32074b44cc 100644 --- a/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs @@ -11,10 +11,22 @@ using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { + /// + /// Representation of the media type header. See . + /// public class MediaTypeHeaderValue { - private const string CharsetString = "charset"; private const string BoundaryString = "boundary"; + private const string CharsetString = "charset"; + private const string MatchesAllString = "*/*"; + private const string QualityString = "q"; + private const string WildcardString = "*"; + + private const char ForwardSlashCharacter = '/'; + private const char PeriodCharacter = '.'; + private const char PlusCharacter = '+'; + + private static readonly char[] PeriodCharacterArray = new char[] { PeriodCharacter }; private static readonly HttpHeaderParser SingleValueParser = new GenericHeaderParser(false, GetMediaTypeLength); @@ -31,18 +43,33 @@ namespace Microsoft.Net.Http.Headers // Used by the parser to create a new instance of this type. } + /// + /// Initializes a instance. + /// + /// A representation of a media type. + /// The text provided must be a single media type without parameters. public MediaTypeHeaderValue(StringSegment mediaType) { - CheckMediaTypeFormat(mediaType, "mediaType"); + CheckMediaTypeFormat(mediaType, nameof(mediaType)); _mediaType = mediaType; } + /// + /// Initializes a instance. + /// + /// A representation of a media type. + /// The text provided must be a single media type without parameters. + /// The with the quality of the media type. public MediaTypeHeaderValue(StringSegment mediaType, double quality) : this(mediaType) { Quality = quality; } + /// + /// Gets or sets the value of the charset parameter. Returns + /// if there is no charset. + /// public StringSegment Charset { get @@ -77,6 +104,10 @@ namespace Microsoft.Net.Http.Headers } } + /// + /// Gets or sets the value of the Encoding parameter. Setting the Encoding will set + /// the to . + /// public Encoding Encoding { get @@ -109,6 +140,10 @@ namespace Microsoft.Net.Http.Headers } } + /// + /// Gets or sets the value of the boundary parameter. Returns + /// if there is no boundary. + /// public StringSegment Boundary { get @@ -141,6 +176,10 @@ namespace Microsoft.Net.Http.Headers } } + /// + /// Gets or sets the media type's parameters. Returns an empty + /// if there are no parameters. + /// public IList Parameters { get @@ -160,6 +199,10 @@ namespace Microsoft.Net.Http.Headers } } + /// + /// Gets or sets the value of the quality parameter. Returns null + /// if there is no quality. + /// public double? Quality { get { return HeaderUtilities.GetQuality(_parameters); } @@ -170,55 +213,155 @@ namespace Microsoft.Net.Http.Headers } } + /// + /// Gets or sets the value of the media type. Returns + /// if there is no media type. + /// + /// + /// For the media type "application/json", the property gives the value + /// "application/json". + /// public StringSegment MediaType { get { return _mediaType; } set { HeaderUtilities.ThrowIfReadOnly(IsReadOnly); - CheckMediaTypeFormat(value, "value"); + CheckMediaTypeFormat(value, nameof(value)); _mediaType = value; } } + /// + /// Gets the type of the . + /// + /// + /// For the media type "application/json", the property gives the value "application". + /// + /// See for more details on the type. public StringSegment Type { get { - return _mediaType.Subsegment(0, _mediaType.IndexOf('/')); + return _mediaType.Subsegment(0, _mediaType.IndexOf(ForwardSlashCharacter)); } } + /// + /// Gets the subtype of the . + /// + /// + /// For the media type "application/vnd.example+json", the property gives the value + /// "vnd.example+json". + /// + /// See for more details on the subtype. public StringSegment SubType { get { - return _mediaType.Subsegment(_mediaType.IndexOf('/') + 1); + return _mediaType.Subsegment(_mediaType.IndexOf(ForwardSlashCharacter) + 1); } } /// - /// MediaType = "*/*" + /// Gets subtype of the , excluding any structured syntax suffix. Returns + /// if there is no subtype without suffix. /// - public bool MatchesAllTypes + /// + /// For the media type "application/vnd.example+json", the property gives the value + /// "vnd.example". + /// + public StringSegment SubTypeWithoutSuffix { get { - return MediaType.Equals("*/*", StringComparison.Ordinal); + var subType = SubType; + var startOfSuffix = subType.LastIndexOf(PlusCharacter); + if (startOfSuffix == -1) + { + return subType; + } + else + { + return subType.Subsegment(0, startOfSuffix); + } } } /// - /// SubType = "*" + /// Gets the structured syntax suffix of the if it has one. + /// See The RFC documentation on structured syntaxes. /// - public bool MatchesAllSubTypes + /// + /// For the media type "application/vnd.example+json", the property gives the value + /// "json". + /// + public StringSegment Suffix { get { - return SubType.Equals("*", StringComparison.Ordinal); + var subType = SubType; + var startOfSuffix = subType.LastIndexOf(PlusCharacter); + if (startOfSuffix == -1) + { + return default(StringSegment); + } + else + { + return subType.Subsegment(startOfSuffix + 1); + } } } + + /// + /// Get a of facets of the . Facets are a + /// period separated list of StringSegments in the . + /// See The RFC documentation on facets. + /// + /// + /// For the media type "application/vnd.example+json", the property gives the value: + /// {"vnd", "example"} + /// + public IEnumerable Facets + { + get + { + return SubTypeWithoutSuffix.Split(PeriodCharacterArray); + } + } + + /// + /// Gets whether this matches all types. + /// + public bool MatchesAllTypes => MediaType.Equals(MatchesAllString, StringComparison.Ordinal); + + /// + /// 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(WildcardString, StringComparison.Ordinal); + + /// + /// 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(WildcardString, StringComparison.OrdinalIgnoreCase); + + /// + /// Gets whether the is readonly. + /// public bool IsReadOnly { get { return _isReadOnly; } @@ -247,56 +390,14 @@ namespace Microsoft.Net.Http.Headers } // "text/plain" is a subset of "text/plain", "text/*" and "*/*". "*/*" is a subset only of "*/*". - if (!Type.Equals(otherMediaType.Type, comparisonType: StringComparison.OrdinalIgnoreCase)) - { - if (!otherMediaType.MatchesAllTypes) - { - return false; - } - } - else if (!SubType.Equals(otherMediaType.SubType, comparisonType: StringComparison.OrdinalIgnoreCase)) - { - if (!otherMediaType.MatchesAllSubTypes) - { - return false; - } - } - - // "text/plain; charset=utf-8; level=1" is a subset of "text/plain; charset=utf-8". In turn - // "text/plain; charset=utf-8" is a subset of "text/plain". - if (otherMediaType._parameters != null && otherMediaType._parameters.Count != 0) - { - // Make sure all parameters in the potential superset are included locally. Fine to have additional - // parameters locally; they make this one more specific. - foreach (var parameter in otherMediaType._parameters) - { - if (parameter.Name.Equals("q", StringComparison.OrdinalIgnoreCase)) - { - // "q" and later parameters are not involved in media type matching. Quoting the RFC: The first - // "q" parameter (if any) separates the media-range parameter(s) from the accept-params. - break; - } - - var localParameter = NameValueHeaderValue.Find(_parameters, parameter.Name); - if (localParameter == null) - { - // Not found. - return false; - } - - if (!StringSegment.Equals(parameter.Value, localParameter.Value, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - } - - return true; + return MatchesType(otherMediaType) && + MatchesSubtype(otherMediaType) && + MatchesParameters(otherMediaType); } /// /// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components, - /// while avoiding the cost of revalidating the components. + /// while avoiding the cost of re-validating the components. /// /// A deep copy. public MediaTypeHeaderValue Copy() @@ -314,7 +415,7 @@ namespace Microsoft.Net.Http.Headers /// /// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components, - /// while avoiding the cost of revalidating the components. This copy is read-only. + /// while avoiding the cost of re-validating the components. This copy is read-only. /// /// A deep, read-only, copy. public MediaTypeHeaderValue CopyAsReadOnly() @@ -362,33 +463,67 @@ namespace Microsoft.Net.Http.Headers return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_mediaType) ^ NameValueHeaderValue.GetHashCode(_parameters); } + /// + /// Takes a media type and parses it into the and its associated parameters. + /// + /// The with the media type. + /// The parsed . public static MediaTypeHeaderValue Parse(StringSegment input) { var index = 0; return SingleValueParser.ParseValue(input, ref index); } + /// + /// Takes a media type, which can include parameters, and parses it into the and its associated parameters. + /// + /// The with the media type. The media type constructed here must not have an y + /// The parsed + /// True if the value was successfully parsed. public static bool TryParse(StringSegment input, out MediaTypeHeaderValue parsedValue) { var index = 0; return SingleValueParser.TryParseValue(input, ref index, out parsedValue); } + /// + /// Takes an of and parses it into the and its associated parameters. + /// + /// A list of media types + /// The parsed . public static IList ParseList(IList inputs) { return MultipleValueParser.ParseValues(inputs); } + /// + /// Takes an of and parses it into the and its associated parameters. + /// Throws if there is invalid data in a string. + /// + /// A list of media types + /// The parsed . public static IList ParseStrictList(IList inputs) { return MultipleValueParser.ParseStrictValues(inputs); } + /// + /// Takes an of and parses it into the and its associated parameters. + /// + /// A list of media types + /// The parsed . + /// True if the value was successfully parsed. public static bool TryParseList(IList inputs, out IList parsedValues) { return MultipleValueParser.TryParseValues(inputs, out parsedValues); } + /// + /// Takes an of and parses it into the and its associated parameters. + /// + /// A list of media types + /// The parsed . + /// True if the value was successfully parsed. public static bool TryParseStrictList(IList inputs, out IList parsedValues) { return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); @@ -481,7 +616,7 @@ namespace Microsoft.Net.Http.Headers } else { - mediaType = input.Substring(startIndex, typeLength) + "/" + input.Substring(current, subtypeLength); + mediaType = input.Substring(startIndex, typeLength) + ForwardSlashCharacter + input.Substring(current, subtypeLength); } return mediaTypeLength; @@ -502,5 +637,85 @@ namespace Microsoft.Net.Http.Headers throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid media type '{0}'.", mediaType)); } } + + private bool MatchesType(MediaTypeHeaderValue set) + { + return set.MatchesAllTypes || + set.Type.Equals(Type, StringComparison.OrdinalIgnoreCase); + } + + private bool MatchesSubtype(MediaTypeHeaderValue set) + { + if (set.MatchesAllSubTypes) + { + return true; + } + if (set.Suffix.HasValue) + { + if (Suffix.HasValue) + { + return MatchesSubtypeWithoutSuffix(set) && MatchesSubtypeSuffix(set); + } + else + { + return false; + } + } + else + { + return set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase); + } + } + + private bool MatchesSubtypeWithoutSuffix(MediaTypeHeaderValue set) + { + return set.MatchesAllSubTypesWithoutSuffix || + set.SubTypeWithoutSuffix.Equals(SubTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase); + } + + private bool MatchesParameters(MediaTypeHeaderValue set) + { + if (set._parameters != null && set._parameters.Count != 0) + { + // Make sure all parameters in the potential superset are included locally. Fine to have additional + // parameters locally; they make this one more specific. + foreach (var parameter in set._parameters) + { + if (parameter.Name.Equals(WildcardString, StringComparison.OrdinalIgnoreCase)) + { + // 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; + } + + if (parameter.Name.Equals(QualityString, StringComparison.OrdinalIgnoreCase)) + { + // "q" and later parameters are not involved in media type matching. Quoting the RFC: The first + // "q" parameter (if any) separates the media-range parameter(s) from the accept-params. + break; + } + + var localParameter = NameValueHeaderValue.Find(_parameters, parameter.Name); + if (localParameter == null) + { + // Not found. + return false; + } + + if (!StringSegment.Equals(parameter.Value, localParameter.Value, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + } + return true; + } + + private bool MatchesSubtypeSuffix(MediaTypeHeaderValue 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.Suffix.Equals(Suffix, StringComparison.OrdinalIgnoreCase); + } } } diff --git a/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValueComparer.cs b/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValueComparer.cs index 21ab21f71e..cc34640988 100644 --- a/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValueComparer.cs +++ b/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValueComparer.cs @@ -28,11 +28,15 @@ namespace Microsoft.Net.Http.Headers /// /// Performs comparisons based on the arguments' quality values /// (aka their "q-value"). Values with identical q-values are considered equal (i.e. the result is 0) - /// with the exception that subtype wildcards are considered less than specific media types and full - /// wildcards are considered less than subtype wildcards. This allows callers to sort a sequence of - /// following their q-values in the order of specific - /// media types, subtype wildcards, and last any full wildcards. + /// with the exception that suffixed subtype wildcards are considered less than subtype wildcards, subtype wildcards + /// are considered less than specific media types and full wildcards are considered less than + /// subtype wildcards. This allows callers to sort a sequence of following + /// their q-values in the order of specific media types, subtype wildcards, and last any full wildcards. /// + /// + /// If we had a list of media types (comma separated): { text/*;q=0.8, text/*+json;q=0.8, */*;q=1, */*;q=0.8, text/plain;q=0.8 } + /// Sorting them using Compare would return: { */*;q=0.8, text/*;q=0.8, text/*+json;q=0.8, text/plain;q=0.8, */*;q=1 } + /// public int Compare(MediaTypeHeaderValue mediaType1, MediaTypeHeaderValue mediaType2) { if (object.ReferenceEquals(mediaType1, mediaType2)) @@ -62,6 +66,14 @@ namespace Microsoft.Net.Http.Headers { return 1; } + else if (mediaType1.MatchesAllSubTypesWithoutSuffix && !mediaType2.MatchesAllSubTypesWithoutSuffix) + { + return -1; + } + else if (!mediaType1.MatchesAllSubTypesWithoutSuffix && mediaType2.MatchesAllSubTypesWithoutSuffix) + { + return 1; + } } else if (!mediaType1.SubType.Equals(mediaType2.SubType, StringComparison.OrdinalIgnoreCase)) { @@ -73,6 +85,25 @@ namespace Microsoft.Net.Http.Headers { return 1; } + else if (mediaType1.MatchesAllSubTypesWithoutSuffix && !mediaType2.MatchesAllSubTypesWithoutSuffix) + { + return -1; + } + else if (!mediaType1.MatchesAllSubTypesWithoutSuffix && mediaType2.MatchesAllSubTypesWithoutSuffix) + { + return 1; + } + } + else if (!mediaType1.Suffix.Equals(mediaType2.Suffix, StringComparison.OrdinalIgnoreCase)) + { + if (mediaType1.MatchesAllSubTypesWithoutSuffix) + { + return -1; + } + else if (mediaType2.MatchesAllSubTypesWithoutSuffix) + { + return 1; + } } } diff --git a/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueComparerTests.cs b/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueComparerTests.cs index 44d723cd9e..3ce2702ec6 100644 --- a/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueComparerTests.cs +++ b/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueComparerTests.cs @@ -18,8 +18,10 @@ namespace Microsoft.Net.Http.Headers { "application/*", "text/plain", + "text/*+json;q=0.8", "text/plain;q=1.0", "text/plain", + "text/*+json;q=0.6", "text/plain;q=0", "*/*;q=0.8", "*/*;q=1", @@ -27,6 +29,7 @@ namespace Microsoft.Net.Http.Headers "text/plain;q=0.8", "text/*;q=0.8", "text/*;q=0.6", + "text/*+json;q=0.4", "text/*;q=1.0", "*/*;q=0.4", "text/plain;q=0.6", @@ -43,10 +46,13 @@ namespace Microsoft.Net.Http.Headers "text/*;q=1.0", "*/*;q=1", "text/plain;q=0.8", + "text/*+json;q=0.8", "text/*;q=0.8", "*/*;q=0.8", "text/plain;q=0.6", + "text/*+json;q=0.6", "text/*;q=0.6", + "text/*+json;q=0.4", "*/*;q=0.4", "text/plain;q=0", } diff --git a/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs index 1d5c9e9f65..75cccabc9c 100644 --- a/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs +++ b/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.Primitives; using Xunit; namespace Microsoft.Net.Http.Headers @@ -39,6 +40,68 @@ namespace Microsoft.Net.Http.Headers AssertFormatException("text/plain;charset=utf-8"); // ctor takes only media-type name, no parameters } + public static TheoryData MediaTypesWithSuffixes => + new TheoryData + { + // See https://tools.ietf.org/html/rfc6838#section-4.2 for allowed names spec + { "application/json", "json", null }, + { "application/json+", "json", "" }, + { "application/+json", "", "json" }, + { "application/entitytype+json", "entitytype", "json" }, + { "applica+tion/entitytype+json", "entitytype", "json" }, + }; + + [Theory] + [MemberData(nameof(MediaTypesWithSuffixes))] + public void Ctor_CanParseSuffixedMediaTypes(string mediaType, string expectedSubTypeWithoutSuffix, string expectedSubTypeSuffix) + { + var result = new MediaTypeHeaderValue(mediaType); + + Assert.Equal(new StringSegment(expectedSubTypeWithoutSuffix), result.SubTypeWithoutSuffix); // TODO consider overloading to have SubTypeWithoutSuffix? + Assert.Equal(new StringSegment(expectedSubTypeSuffix), result.Suffix); + } + + public static TheoryData MediaTypesWithSuffixesAndSpaces => + new TheoryData + { + // See https://tools.ietf.org/html/rfc6838#section-4.2 for allowed names spec + { " application / json+xml", "json", "xml" }, + { " application / vnd.com-pany.some+entity!.v2+js.#$&^_n ; q=\"0.3+1\"", "vnd.com-pany.some+entity!.v2", "js.#$&^_n"}, + { " application/ +json", "", "json" }, + { " application/ entitytype+json ", "entitytype", "json" }, + { " applica+tion/ entitytype+json ", "entitytype", "json" } + }; + + [Theory] + [MemberData(nameof(MediaTypesWithSuffixesAndSpaces))] + public void Parse_CanParseSuffixedMediaTypes(string mediaType, string expectedSubTypeWithoutSuffix, string expectedSubTypeSuffix) + { + var result = MediaTypeHeaderValue.Parse(mediaType); + + Assert.Equal(new StringSegment(expectedSubTypeWithoutSuffix), result.SubTypeWithoutSuffix); // TODO consider overloading to have SubTypeWithoutSuffix? + Assert.Equal(new StringSegment(expectedSubTypeSuffix), result.Suffix); + } + + [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 MediaTypeHeaderValue(value); + + // Act + var result = mediaType.MatchesAllSubTypesWithoutSuffix; + + // Assert + Assert.Equal(expectedReturnValue, result); + } + [Fact] public void Ctor_MediaTypeValidFormat_SuccessfullyCreated() { @@ -644,6 +707,8 @@ namespace Microsoft.Net.Http.Headers [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;foo=bar;q=0.0;charset=utf-8")] // different order of parameters [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/*;charset=utf-8;foo=bar;q=0.0")] [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "*/*;charset=utf-8;foo=bar;q=0.0")] + [InlineData("application/json;v=2", "application/json;*")] + [InlineData("application/json;v=2;charset=utf-8", "application/json;v=2;*")] public void IsSubsetOf_PositiveCases(string mediaType1, string mediaType2) { // Arrange @@ -681,6 +746,74 @@ namespace Microsoft.Net.Http.Headers Assert.False(isSubset); } + [Theory] + [InlineData("application/entity+json", "application/entity+json")] + [InlineData("application/*+json", "application/entity+json")] + [InlineData("application/*+json", "application/*+json")] + [InlineData("application/*", "application/*+JSON")] + [InlineData("application/vnd.github+json", "application/vnd.github+json")] + [InlineData("application/*", "application/entity+JSON")] + [InlineData("*/*", "application/entity+json")] + public void IsSubsetOfWithSuffixes_PositiveCases(string set, string subset) + { + // Arrange + var setMediaType = MediaTypeHeaderValue.Parse(set); + var subSetMediaType = MediaTypeHeaderValue.Parse(subset); + + // Act + var result = subSetMediaType.IsSubsetOf(setMediaType); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("application/entity+json", "application/entity+txt")] + [InlineData("application/entity+json", "application/entity.v2+json")] + [InlineData("application/*+json", "application/entity+txt")] + [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 IsSubSetOfWithSuffixes_NegativeCases(string set, string subset) + { + // Arrange + var setMediaType = MediaTypeHeaderValue.Parse(set); + var subSetMediaType = MediaTypeHeaderValue.Parse(subset); + + // Act + var result = subSetMediaType.IsSubsetOf(setMediaType); + + // Assert + Assert.False(result); + } + + public static TheoryData> MediaTypesWithFacets => + new TheoryData> + { + { "application/vdn.github", + new List(){ "vdn", "github" } }, + { "application/vdn.github+json", + new List(){ "vdn", "github" } }, + { "application/vdn.github.v3+json", + new List(){ "vdn", "github", "v3" } }, + { "application/vdn.github.+json", + new List(){ "vdn", "github", "" } }, + }; + + [Theory] + [MemberData(nameof(MediaTypesWithFacets))] + public void Facets_TestPositiveCases(string input, List expected) + { + // Arrange + var mediaType = MediaTypeHeaderValue.Parse(input); + + // Act + var result = mediaType.Facets; + + // Assert + Assert.Equal(expected, result); + } + private void CheckValidParse(string input, MediaTypeHeaderValue expectedResult) { var result = MediaTypeHeaderValue.Parse(input);