最近在项目上使用了ASP.NET WebAPI 代替原有使用MVC3开发的接口, 原因就是因为WebAPI支持SelftHost, 在安装到客户端时无需安装IIS, 免去很多麻烦.
但是发布后好多童鞋不明白WebAPI的自动格式转换规则是怎样的, 所以在这里特别拿出来说明一下.
简单说明:
规则以优先级排序:
1.MatchOnRequestWithMediaTypeMapping, 即没有Accept头, 或者只有一个并为 */*, 这种情况能够匹配任何Formatter
2.MatchOnRequestAcceptHeader(这里三种头部优先级一样), 即Accept匹配, */*, text/json, application/json, 如果为*/*则匹配所有Formatter, 如果Formatter支持的类型为text/json,
text/json即大类与小类都与Formatter匹配, 如果Formatter支持的类型为text/json, accept为application/json即小类匹配
3.MatchOnRequestMediaType, 即没有Accept头匹配, 但Content Accept头匹配
4.MatchOnCanWriteType, 即最勉强的匹配
5.如果同样类型的匹配出现多个, 即比较他们的Quality值, 最大值优先
详细说明开始
1.首先在执行完Action后, WebAPI会调用ValueResultConverter类的Converter方法对Action的返回值进行序列化操作, 以下红色代码就是会将返回值转换为一个HttpResponseMessage对象
1 public class ValueResultConverter<T> : IActionResultConverter 2 { 3 public HttpResponseMessage Convert(HttpControllerContext controllerContext, object actionResult) 4 { 5 if (controllerContext == null) 6 { 7 throw Error.ArgumentNull("controllerContext"); 8 } 9 10 HttpResponseMessage resultAsResponse = actionResult as HttpResponseMessage; 11 if (resultAsResponse != null) 12 { 13 resultAsResponse.EnsureResponseHasRequest(controllerContext.Request); 14 return resultAsResponse; 15 } 16 17 T value = (T)actionResult; 18 return controllerContext.Request.CreateResponse<T>(HttpStatusCode.OK, value, controllerContext.Configuration); 19 } 20 }
2.之后将会进入DefaultContentNegotiator类的Negotiate方法, 我们可以看到标红的两句代码, ComputeFormatterMatches方法传入要转换的类型, 请求对象与所有的Formatter, 来获取所有匹配的Formatter,
在这里大家应该可以预想到, 如何匹配Formatter就是根据要转换对象的类型, 请求对象的属性, 还有所有格式化器决定的.
我们可以看到ComputeFormatterMatches方法返回的是集合对象, 但是最终我们只会返回一个结果, 因此只能找出一个最为匹配的Formatter, 因此程序还会执行以下代码来找到最匹配的Formatter.
MediaTypeFormatterMatch bestFormatterMatch = SelectResponseMediaTypeFormatter(matches);
public virtual ContentNegotiationResult Negotiate(Type type, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters) { if (type == null) { throw Error.ArgumentNull("type"); } if (request == null) { throw Error.ArgumentNull("request"); } if (formatters == null) { throw Error.ArgumentNull("formatters"); } // If formatter list is empty then we won't find a match if (!formatters.Any()) { return null; } // Go through each formatter to compute how well it matches. Collection<MediaTypeFormatterMatch> 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; }
接下来我们进入到ComputeFormatterMatches方法中, 来查看一下究竟是如何判断返回对象与哪个Formatter匹配的.
protected virtual Collection<MediaTypeFormatterMatch> ComputeFormatterMatches(Type type, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters) { if (type == null) { throw Error.ArgumentNull("type"); } if (request == null) { throw Error.ArgumentNull("request"); } if (formatters == null) { throw Error.ArgumentNull("formatters"); } IEnumerable<MediaTypeWithQualityHeaderValue> sortedAcceptValues = null; // Go through each formatter to find how well it matches. Collection<MediaTypeFormatterMatch> matches = new Collection<MediaTypeFormatterMatch>(); foreach (MediaTypeFormatter formatter in formatters) { 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; }
foreach (MediaTypeFormatter formatter in formatters)会遍历所有Formatter, 在循环中进行以下匹配判断:
1. formatter.CanWriteType(type), 判断类型是否为Formatter所支持的类型, 如果不是则继续下一循环.
2.MatchMediaTypeMapping(request, formatter), 大家可以查看以下代码的IF判断, 如果请求中不包含Accept头, 或者仅有一个并且为*/*时, 证明该Formatter匹配
public override double TryMatchMediaType(HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } // Accept header trumps XHR mapping. // Accept: */* is equivalent to passing no Accept header. if (request.Headers.Accept.Count == 0 || (request.Headers.Accept.Count == 1 && request.Headers.Accept.First().MediaType.Equals("*/*", StringComparison.Ordinal))) { return base.TryMatchMediaType(request); } else { return FormattingUtilities.NoMatch; } }
3.MatchAcceptHeader(sortedAcceptValues, formatter), 判断Accept头是否与Formatter匹配
protected virtual MediaTypeFormatterMatch MatchAcceptHeader(IEnumerable<MediaTypeWithQualityHeaderValue> sortedAcceptValues, MediaTypeFormatter formatter) { if (sortedAcceptValues == null) { throw Error.ArgumentNull("sortedAcceptValues"); } if (formatter == null) { throw Error.ArgumentNull("formatter"); } foreach (MediaTypeWithQualityHeaderValue acceptMediaTypeValue in sortedAcceptValues) { foreach (MediaTypeHeaderValue supportedMediaType in formatter.SupportedMediaTypes) { 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; }
4. MatchRequestMediaType(request, formatter), 判断内容头部是否与Formatter匹配
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) { foreach (MediaTypeHeaderValue supportedMediaType in formatter.SupportedMediaTypes) { if (supportedMediaType != null && supportedMediaType.IsSubsetOf(requestMediaType)) { return new MediaTypeFormatterMatch(formatter, supportedMediaType, FormattingUtilities.Match, MediaTypeFormatterMatchRanking.MatchOnRequestMediaType); } } } } return null; }
5.如果以上都不匹配, 则判断在配置上想要返回406错误, 还是强行使用Formatter进行格式化
protected virtual bool ShouldMatchOnType(IEnumerable<MediaTypeWithQualityHeaderValue> sortedAcceptValues) { if (sortedAcceptValues == null) { throw Error.ArgumentNull("sortedAcceptValues"); } return !(ExcludeMatchOnTypeOnly && sortedAcceptValues.Any()); }
最后就要找出最为匹配的Formatter了, 代码我就不详细描述了, 规则以优先级排序:
1.MatchOnRequestWithMediaTypeMapping, 即没有Accept头, 或者只有一个并为 */*
2.MatchOnRequestAcceptHeader(这里三种头部优先级一样), 即Accept匹配, */*, text/json, application/json
3.MatchOnRequestMediaType, 即没有Accept头匹配, 但Content Accept头匹配
4.MatchOnCanWriteType, 即最勉强的匹配
5.如果同样类型的匹配出现多个, 即比较他们的Quality值, 最大值优先
protected virtual MediaTypeFormatterMatch SelectResponseMediaTypeFormatter(ICollection<MediaTypeFormatterMatch> matches) { if (matches == null) { throw Error.ArgumentNull("matches"); } 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. foreach (MediaTypeFormatterMatch match in matches) { 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; }