// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.Contracts; using System.Linq; using System.Net.Http.Headers; using System.Text; using System.Web.Http; namespace System.Net.Http.Formatting { /// /// Class that selects a for an /// or . /// public class DefaultContentNegotiator : IContentNegotiator { public DefaultContentNegotiator() : this(false) { } /// /// Initializes a new instance of the with /// the given setting for . /// /// /// If ExcludeMatchOnTypeOnly is true then we don't match on type only which means /// that we return null if we can't match on anything in the request. This is useful /// for generating 406 (Not Acceptable) status codes. /// public DefaultContentNegotiator(bool excludeMatchOnTypeOnly) { ExcludeMatchOnTypeOnly = excludeMatchOnTypeOnly; } /// /// If ExcludeMatchOnTypeOnly is true then we don't match on type only which means /// that we return null if we can't match on anything in the request. This is useful /// for generating 406 (Not Acceptable) status codes. /// public bool ExcludeMatchOnTypeOnly { get; private set; } /// /// Performs content negotiating by selecting the most appropriate out of the passed in /// for the given that can serialize an object of the given /// . /// /// The type to be serialized. /// The request. /// The set of objects from which to choose. /// The result of the negotiation containing the most appropriate instance, /// or null if there is no appropriate formatter. public virtual ContentNegotiationResult Negotiate(Type type, HttpRequestMessage request, IEnumerable formatters) { // Performance-sensitive if (type == null) { throw Error.ArgumentNull("type"); } if (request == null) { throw Error.ArgumentNull("request"); } if (formatters == null) { throw Error.ArgumentNull("formatters"); } // Go through each formatter to compute how well it matches. Collection matches = ComputeFormatterMatches(type, request, formatters); // Select best formatter match among the matches MediaTypeFormatterMatch bestFormatterMatch = SelectResponseMediaTypeFormatter(matches); // We found a best formatter if (bestFormatterMatch != null) { // Find the best character encoding for the selected formatter Encoding bestEncodingMatch = SelectResponseCharacterEncoding(request, bestFormatterMatch.Formatter); if (bestEncodingMatch != null) { bestFormatterMatch.MediaType.CharSet = bestEncodingMatch.WebName; } MediaTypeHeaderValue bestMediaType = bestFormatterMatch.MediaType; MediaTypeFormatter bestFormatter = bestFormatterMatch.Formatter.GetPerRequestFormatterInstance(type, request, bestMediaType); return new ContentNegotiationResult(bestFormatter, bestMediaType); } return null; } /// /// Determine how well each formatter matches by associating a value /// with the formatter. Then associate the quality of the match based on q-factors and other parameters. The result of this /// method is a collection of the matches found categorized and assigned a quality value. /// /// The type to be serialized. /// The request. /// The set of objects from which to choose. /// A collection containing all the matches. protected virtual Collection ComputeFormatterMatches(Type type, HttpRequestMessage request, IEnumerable formatters) { // Performance-sensitive if (type == null) { throw Error.ArgumentNull("type"); } if (request == null) { throw Error.ArgumentNull("request"); } if (formatters == null) { throw Error.ArgumentNull("formatters"); } IEnumerable sortedAcceptValues = null; // Go through each formatter to find how well it matches. ListWrapperCollection matches = new ListWrapperCollection(); MediaTypeFormatter[] writingFormatters = GetWritingFormatters(formatters); for (int i = 0; i < writingFormatters.Length; i++) { MediaTypeFormatter formatter = writingFormatters[i]; MediaTypeFormatterMatch match = null; // Check first that formatter can write the actual type if (!formatter.CanWriteType(type)) { // Formatter can't even write the type so no match at all continue; } // Match against media type mapping. if ((match = MatchMediaTypeMapping(request, formatter)) != null) { matches.Add(match); continue; } // Match against the accept header values. if (sortedAcceptValues == null) { // Sort the Accept header values in descending order based on q-factor sortedAcceptValues = SortMediaTypeWithQualityHeaderValuesByQFactor(request.Headers.Accept); } if ((match = MatchAcceptHeader(sortedAcceptValues, formatter)) != null) { matches.Add(match); continue; } // Match against request's media type if any if ((match = MatchRequestMediaType(request, formatter)) != null) { matches.Add(match); continue; } // Check whether we should match on type or stop the matching process. // The latter is used to generate 406 (Not Acceptable) status codes. bool shouldMatchOnType = ShouldMatchOnType(sortedAcceptValues); // Match against the type of object we are writing out if (shouldMatchOnType && (match = MatchType(type, formatter)) != null) { matches.Add(match); continue; } } return matches; } /// /// Select the best match among the candidate matches found. /// /// The collection of matches. /// The determined to be the best match. protected virtual MediaTypeFormatterMatch SelectResponseMediaTypeFormatter(ICollection matches) { // Performance-sensitive if (matches == null) { throw Error.ArgumentNull("matches"); } List matchList = matches.AsList(); MediaTypeFormatterMatch bestMatchOnType = null; MediaTypeFormatterMatch bestMatchOnAcceptHeaderLiteral = null; MediaTypeFormatterMatch bestMatchOnAcceptHeaderSubtypeMediaRange = null; MediaTypeFormatterMatch bestMatchOnAcceptHeaderAllMediaRange = null; MediaTypeFormatterMatch bestMatchOnMediaTypeMapping = null; MediaTypeFormatterMatch bestMatchOnRequestMediaType = null; // Go through each formatter to find the best match in each category. for (int i = 0; i < matchList.Count; i++) { MediaTypeFormatterMatch match = matchList[i]; switch (match.Ranking) { case MediaTypeFormatterMatchRanking.MatchOnCanWriteType: // First match by type trumps all other type matches if (bestMatchOnType == null) { bestMatchOnType = match; } break; case MediaTypeFormatterMatchRanking.MatchOnRequestWithMediaTypeMapping: // Matches on accept headers using mappings must choose the highest quality match bestMatchOnMediaTypeMapping = UpdateBestMatch(bestMatchOnMediaTypeMapping, match); break; case MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral: // Matches on accept headers must choose the highest quality match. // A match of 0.0 means we won't use it at all. bestMatchOnAcceptHeaderLiteral = UpdateBestMatch(bestMatchOnAcceptHeaderLiteral, match); break; case MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange: // Matches on accept headers must choose the highest quality match. // A match of 0.0 means we won't use it at all. bestMatchOnAcceptHeaderSubtypeMediaRange = UpdateBestMatch(bestMatchOnAcceptHeaderSubtypeMediaRange, match); break; case MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderAllMediaRange: // Matches on accept headers must choose the highest quality match. // A match of 0.0 means we won't use it at all. bestMatchOnAcceptHeaderAllMediaRange = UpdateBestMatch(bestMatchOnAcceptHeaderAllMediaRange, match); break; case MediaTypeFormatterMatchRanking.MatchOnRequestMediaType: // First match on request content type trumps other request content matches if (bestMatchOnRequestMediaType == null) { bestMatchOnRequestMediaType = match; } break; } } // If we received matches based on both supported media types and from media type mappings, // we want to give precedence to the media type mappings, but only if their quality is >= that of the supported media type. // We do this because media type mappings are the user's extensibility point and must take precedence over normal // supported media types in the case of a tie. The 99% case is where both have quality 1.0. if (bestMatchOnMediaTypeMapping != null) { MediaTypeFormatterMatch mappingOverride = bestMatchOnMediaTypeMapping; mappingOverride = UpdateBestMatch(mappingOverride, bestMatchOnAcceptHeaderLiteral); mappingOverride = UpdateBestMatch(mappingOverride, bestMatchOnAcceptHeaderSubtypeMediaRange); mappingOverride = UpdateBestMatch(mappingOverride, bestMatchOnAcceptHeaderAllMediaRange); if (mappingOverride != bestMatchOnMediaTypeMapping) { bestMatchOnMediaTypeMapping = null; } } // now select the formatter and media type // A MediaTypeMapping is highest precedence -- it is an extensibility point // allowing the user to override normal accept header matching MediaTypeFormatterMatch bestMatch = null; if (bestMatchOnMediaTypeMapping != null) { bestMatch = bestMatchOnMediaTypeMapping; } else if (bestMatchOnAcceptHeaderLiteral != null || bestMatchOnAcceptHeaderSubtypeMediaRange != null || bestMatchOnAcceptHeaderAllMediaRange != null) { bestMatch = UpdateBestMatch(bestMatch, bestMatchOnAcceptHeaderLiteral); bestMatch = UpdateBestMatch(bestMatch, bestMatchOnAcceptHeaderSubtypeMediaRange); bestMatch = UpdateBestMatch(bestMatch, bestMatchOnAcceptHeaderAllMediaRange); } else if (bestMatchOnRequestMediaType != null) { bestMatch = bestMatchOnRequestMediaType; } else if (bestMatchOnType != null) { bestMatch = bestMatchOnType; } return bestMatch; } /// /// Determine the best character encoding for writing the response. First we look /// for accept-charset headers and if not found then we try to match /// any charset encoding in the request (in case of PUT, POST, etc.) /// If no encoding is found then we use the default for the formatter. /// /// The determined to be the best match. protected virtual Encoding SelectResponseCharacterEncoding(HttpRequestMessage request, MediaTypeFormatter formatter) { if (request == null) { throw Error.ArgumentNull("request"); } if (formatter == null) { throw Error.ArgumentNull("formatter"); } // If there are any SupportedEncodings then we pick an encoding List supportedEncodings = formatter.SupportedEncodingsInternal; if (supportedEncodings.Count > 0) { // Sort Accept-Charset header values IEnumerable sortedAcceptCharsetValues = SortStringWithQualityHeaderValuesByQFactor(request.Headers.AcceptCharset); // Check for match based on accept-charset headers foreach (StringWithQualityHeaderValue acceptCharset in sortedAcceptCharsetValues) { for (int i = 0; i < supportedEncodings.Count; i++) { Encoding encoding = supportedEncodings[i]; if (encoding != null && acceptCharset.Quality != FormattingUtilities.NoMatch && (acceptCharset.Value.Equals(encoding.WebName, StringComparison.OrdinalIgnoreCase) || acceptCharset.Value.Equals("*", StringComparison.OrdinalIgnoreCase))) { return encoding; } } } // Check for match based on any request entity body return formatter.SelectCharacterEncoding(request.Content != null ? request.Content.Headers : null); } return null; } /// /// Match a request against the s registered with the formatter. /// /// The request to match. /// The formatter to match against. /// A indicating the quality of the match or null is no match. protected virtual MediaTypeFormatterMatch MatchMediaTypeMapping(HttpRequestMessage request, MediaTypeFormatter formatter) { if (request == null) { throw Error.ArgumentNull("request"); } if (formatter == null) { throw Error.ArgumentNull("formatter"); } List mediaTypeMappings = formatter.MediaTypeMappingsInternal; for (int i = 0; i < mediaTypeMappings.Count; i++) { MediaTypeMapping mapping = mediaTypeMappings[i]; double quality; if (mapping != null && ((quality = mapping.TryMatchMediaType(request)) > FormattingUtilities.NoMatch)) { return new MediaTypeFormatterMatch(formatter, mapping.MediaType, quality, MediaTypeFormatterMatchRanking.MatchOnRequestWithMediaTypeMapping); } } return null; } /// /// Match the request accept header field values against the formatter's registered supported media types. /// /// The sorted accept header values to match. /// The formatter to match against. /// A indicating the quality of the match or null is no match. protected virtual MediaTypeFormatterMatch MatchAcceptHeader(IEnumerable sortedAcceptValues, MediaTypeFormatter formatter) { if (sortedAcceptValues == null) { throw Error.ArgumentNull("sortedAcceptValues"); } if (formatter == null) { throw Error.ArgumentNull("formatter"); } foreach (MediaTypeWithQualityHeaderValue acceptMediaTypeValue in sortedAcceptValues) { List supportedMediaTypes = formatter.SupportedMediaTypesInternal; for (int i = 0; i < supportedMediaTypes.Count; i++) { MediaTypeHeaderValue supportedMediaType = supportedMediaTypes[i]; MediaTypeHeaderValueRange range; if (supportedMediaType != null && acceptMediaTypeValue.Quality != FormattingUtilities.NoMatch && supportedMediaType.IsSubsetOf(acceptMediaTypeValue, out range)) { MediaTypeFormatterMatchRanking ranking; switch (range) { case MediaTypeHeaderValueRange.AllMediaRange: ranking = MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderAllMediaRange; break; case MediaTypeHeaderValueRange.SubtypeMediaRange: ranking = MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange; break; default: ranking = MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral; break; } return new MediaTypeFormatterMatch(formatter, supportedMediaType, acceptMediaTypeValue.Quality, ranking); } } } return null; } /// /// Match any request media type (in case there is a request entity body) against the formatter's registered /// media types. /// /// The request to match. /// The formatter to match against. /// A indicating the quality of the match or null is no match. protected virtual MediaTypeFormatterMatch MatchRequestMediaType(HttpRequestMessage request, MediaTypeFormatter formatter) { if (request == null) { throw Error.ArgumentNull("request"); } if (formatter == null) { throw Error.ArgumentNull("formatter"); } if (request.Content != null) { MediaTypeHeaderValue requestMediaType = request.Content.Headers.ContentType; if (requestMediaType != null) { List supportedMediaTypes = formatter.SupportedMediaTypesInternal; for (int i = 0; i < supportedMediaTypes.Count; i++) { MediaTypeHeaderValue supportedMediaType = supportedMediaTypes[i]; if (supportedMediaType != null && supportedMediaType.IsSubsetOf(requestMediaType)) { return new MediaTypeFormatterMatch(formatter, supportedMediaType, FormattingUtilities.Match, MediaTypeFormatterMatchRanking.MatchOnRequestMediaType); } } } } return null; } /// /// Determine whether to match on type or not. This is used to determine whether to /// generate a 406 response or use the default media type formatter in case there /// is no match against anything in the request. If ExcludeMatchOnTypeOnly is true /// then we don't match on type unless there are no accept headers. /// /// The sorted accept header values to match. /// True if not ExcludeMatchOnTypeOnly and accept headers with a q-factor bigger than 0.0 are present. protected virtual bool ShouldMatchOnType(IEnumerable sortedAcceptValues) { if (sortedAcceptValues == null) { throw Error.ArgumentNull("sortedAcceptValues"); } return !(ExcludeMatchOnTypeOnly && sortedAcceptValues.Any()); } /// /// Pick the first supported media type and indicate we've matched only on type /// /// The type to be serialized. /// The formatter we are matching against. /// A indicating the quality of the match or null is no match. protected virtual MediaTypeFormatterMatch MatchType(Type type, MediaTypeFormatter formatter) { // Performance-sensitive if (type == null) { throw Error.ArgumentNull("type"); } if (formatter == null) { throw Error.ArgumentNull("formatter"); } // We already know that we do match on type -- otherwise we wouldn't even be called -- // so this is just a matter of determining how we match. MediaTypeHeaderValue mediaType = null; List supportedMediaTypes = formatter.SupportedMediaTypesInternal; if (supportedMediaTypes.Count > 0) { mediaType = supportedMediaTypes[0]; } return new MediaTypeFormatterMatch(formatter, mediaType, FormattingUtilities.Match, MediaTypeFormatterMatchRanking.MatchOnCanWriteType); } /// /// Sort Accept header values and related header field values with similar syntax rules /// (if more than 1) in descending order based on q-factor. /// /// The header values to sort. /// The sorted header values. protected virtual IEnumerable SortMediaTypeWithQualityHeaderValuesByQFactor(ICollection headerValues) { if (headerValues == null) { throw Error.ArgumentNull("headerValues"); } if (headerValues.Count > 1) { // Use OrderBy() instead of Array.Sort() as it performs fewer comparisons. In this case the comparisons // are quite expensive so OrderBy() performs better. return headerValues.OrderByDescending(m => m, MediaTypeWithQualityHeaderValueComparer.QualityComparer).ToArray(); } else { return headerValues; } } /// /// Sort Accept-Charset, Accept-Encoding, Accept-Language and related header field values with similar syntax rules /// (if more than 1) in descending order based on q-factor. /// /// The header values to sort. /// The sorted header values. protected virtual IEnumerable SortStringWithQualityHeaderValuesByQFactor(ICollection headerValues) { if (headerValues == null) { throw Error.ArgumentNull("headerValues"); } if (headerValues.Count > 1) { // Use OrderBy() instead of Array.Sort() as it performs fewer comparisons. In this case the comparisons // are quite expensive so OrderBy() performs better. return headerValues.OrderByDescending(m => m, StringWithQualityHeaderValueComparer.QualityComparer).ToArray(); } else { return headerValues; } } /// /// Evaluates whether a match is better than the current match and if so returns the replacement; otherwise returns the /// current match. /// protected virtual MediaTypeFormatterMatch UpdateBestMatch(MediaTypeFormatterMatch current, MediaTypeFormatterMatch potentialReplacement) { if (potentialReplacement == null) { return current; } if (current != null) { return (potentialReplacement.Quality > current.Quality) ? potentialReplacement : current; } return potentialReplacement; } private static MediaTypeFormatter[] GetWritingFormatters(IEnumerable formatters) { Contract.Assert(formatters != null); MediaTypeFormatterCollection formatterCollection = formatters as MediaTypeFormatterCollection; if (formatterCollection != null) { return formatterCollection.WritingFormatters; } return formatters.AsArray(); } } }