Remove Linq and boxed Enumerator allocations from conneg
This commit is contained in:
parent
5ad1a27f6e
commit
1312ed8b71
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
|
|
@ -251,21 +250,20 @@ namespace Microsoft.AspNet.Mvc.Formatters
|
||||||
{
|
{
|
||||||
if (acceptCharsetHeaders != null && acceptCharsetHeaders.Count > 0)
|
if (acceptCharsetHeaders != null && acceptCharsetHeaders.Count > 0)
|
||||||
{
|
{
|
||||||
var sortedAcceptCharsetHeaders = acceptCharsetHeaders
|
var acceptValues = Sort(acceptCharsetHeaders);
|
||||||
.Where(acceptCharset => acceptCharset.Quality != HeaderQuality.NoMatch)
|
for (var i = 0; i < acceptValues.Count; i++)
|
||||||
.OrderByDescending(m => m, StringWithQualityHeaderValueComparer.QualityComparer);
|
|
||||||
|
|
||||||
foreach (var acceptCharset in sortedAcceptCharsetHeaders)
|
|
||||||
{
|
{
|
||||||
var charset = acceptCharset.Value;
|
var charset = acceptValues[i].Value;
|
||||||
if (!string.IsNullOrWhiteSpace(charset))
|
if (!string.IsNullOrWhiteSpace(charset))
|
||||||
{
|
{
|
||||||
var encoding = SupportedEncodings.FirstOrDefault(supportedEncoding =>
|
for (var j = 0; j < SupportedEncodings.Count; j++)
|
||||||
charset.Equals(supportedEncoding.WebName, StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
charset.Equals("*", StringComparison.Ordinal));
|
|
||||||
if (encoding != null)
|
|
||||||
{
|
{
|
||||||
return encoding;
|
var encoding = SupportedEncodings[j];
|
||||||
|
if (charset.Equals(encoding.WebName, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
charset.Equals("*", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return encoding;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -273,5 +271,63 @@ namespace Microsoft.AspNet.Mvc.Formatters
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// There's no allocation-free way to sort an IList and we may have to filter anyway,
|
||||||
|
// so we're going to have to live with the copy + insertion sort.
|
||||||
|
private IList<StringWithQualityHeaderValue> Sort(IList<StringWithQualityHeaderValue> values)
|
||||||
|
{
|
||||||
|
var sortNeeded = false;
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < values.Count; i++)
|
||||||
|
{
|
||||||
|
var value = values[i];
|
||||||
|
if (value.Quality == HeaderQuality.NoMatch)
|
||||||
|
{
|
||||||
|
// Exclude this one
|
||||||
|
}
|
||||||
|
else if (value.Quality != null)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
sortNeeded = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sortNeeded)
|
||||||
|
{
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sorted = new List<StringWithQualityHeaderValue>();
|
||||||
|
for (var i = 0; i < values.Count; i++)
|
||||||
|
{
|
||||||
|
var value = values[i];
|
||||||
|
if (value.Quality == HeaderQuality.NoMatch)
|
||||||
|
{
|
||||||
|
// Exclude this one
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Doing an insertion sort.
|
||||||
|
var position = sorted.BinarySearch(value, StringWithQualityHeaderValueComparer.QualityComparer);
|
||||||
|
if (position >= 0)
|
||||||
|
{
|
||||||
|
sorted.Insert(position + 1, value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sorted.Insert(~position, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want a descending sort, but BinarySearch does ascending
|
||||||
|
sorted.Reverse();
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
|
|
@ -163,7 +162,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
|
||||||
protected virtual IOutputFormatter SelectFormatter(
|
protected virtual IOutputFormatter SelectFormatter(
|
||||||
OutputFormatterWriteContext formatterContext,
|
OutputFormatterWriteContext formatterContext,
|
||||||
IList<MediaTypeHeaderValue> contentTypes,
|
IList<MediaTypeHeaderValue> contentTypes,
|
||||||
IEnumerable<IOutputFormatter> formatters)
|
IList<IOutputFormatter> formatters)
|
||||||
{
|
{
|
||||||
if (formatterContext == null)
|
if (formatterContext == null)
|
||||||
{
|
{
|
||||||
|
|
@ -191,14 +190,15 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
|
||||||
return SelectFormatterUsingAnyAcceptableContentType(formatterContext, formatters, contentTypes);
|
return SelectFormatterUsingAnyAcceptableContentType(formatterContext, formatters, contentTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
var sortedAcceptHeaderMediaTypes = GetSortedAcceptHeaderMediaTypes(formatterContext);
|
var request = formatterContext.HttpContext.Request;
|
||||||
|
var acceptValues = PrepareAcceptValues(request.GetTypedHeaders().Accept);
|
||||||
|
|
||||||
IOutputFormatter selectedFormatter = null;
|
IOutputFormatter selectedFormatter = null;
|
||||||
if (contentTypes == null || contentTypes.Count == 0)
|
if (contentTypes == null || contentTypes.Count == 0)
|
||||||
{
|
{
|
||||||
// Check if we have enough information to do content-negotiation, otherwise get the first formatter
|
// Check if we have enough information to do content-negotiation, otherwise get the first formatter
|
||||||
// which can write the type. Let the formatter choose the Content-Type.
|
// which can write the type. Let the formatter choose the Content-Type.
|
||||||
if (!sortedAcceptHeaderMediaTypes.Any())
|
if (acceptValues == null || acceptValues.Count == 0)
|
||||||
{
|
{
|
||||||
Logger.LogVerbose("No information found on request to perform content negotiation.");
|
Logger.LogVerbose("No information found on request to perform content negotiation.");
|
||||||
|
|
||||||
|
|
@ -213,7 +213,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
|
||||||
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
|
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
|
||||||
formatterContext,
|
formatterContext,
|
||||||
formatters,
|
formatters,
|
||||||
sortedAcceptHeaderMediaTypes);
|
acceptValues);
|
||||||
|
|
||||||
// 2. No formatter was found based on Accept header. Fallback to the first formatter which can write
|
// 2. No formatter was found based on Accept header. Fallback to the first formatter which can write
|
||||||
// the type. Let the formatter choose the Content-Type.
|
// the type. Let the formatter choose the Content-Type.
|
||||||
|
|
@ -230,18 +230,32 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (sortedAcceptHeaderMediaTypes.Any())
|
if (acceptValues != null && acceptValues.Count > 0)
|
||||||
{
|
{
|
||||||
// Filter and remove accept headers which cannot support any of the user specified content types.
|
// Filter and remove accept headers which cannot support any of the user specified content types.
|
||||||
// That is, confirm this result supports a more specific media type than requested e.g. OK if
|
// That is, confirm this result supports a more specific media type than requested e.g. OK if
|
||||||
// "text/*" requested and result supports "text/plain".
|
// "text/*" requested and result supports "text/plain".
|
||||||
var filteredAndSortedAcceptHeaders = sortedAcceptHeaderMediaTypes
|
for (var i = acceptValues.Count - 1; i >= 0; i--)
|
||||||
.Where(acceptHeader => contentTypes.Any(contentType => contentType.IsSubsetOf(acceptHeader)));
|
{
|
||||||
|
var isCompatible = false;
|
||||||
|
for (var j = 0; j < contentTypes.Count; j++)
|
||||||
|
{
|
||||||
|
if (contentTypes[j].IsSubsetOf(acceptValues[i]))
|
||||||
|
{
|
||||||
|
isCompatible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCompatible)
|
||||||
|
{
|
||||||
|
acceptValues.RemoveAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
|
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
|
||||||
formatterContext,
|
formatterContext,
|
||||||
formatters,
|
formatters,
|
||||||
filteredAndSortedAcceptHeaders);
|
acceptValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedFormatter == null)
|
if (selectedFormatter == null)
|
||||||
|
|
@ -275,7 +289,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
|
||||||
/// </returns>
|
/// </returns>
|
||||||
protected virtual IOutputFormatter SelectFormatterNotUsingAcceptHeaders(
|
protected virtual IOutputFormatter SelectFormatterNotUsingAcceptHeaders(
|
||||||
OutputFormatterWriteContext formatterContext,
|
OutputFormatterWriteContext formatterContext,
|
||||||
IEnumerable<IOutputFormatter> formatters)
|
IList<IOutputFormatter> formatters)
|
||||||
{
|
{
|
||||||
if (formatterContext == null)
|
if (formatterContext == null)
|
||||||
{
|
{
|
||||||
|
|
@ -315,8 +329,8 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
|
||||||
/// </returns>
|
/// </returns>
|
||||||
protected virtual IOutputFormatter SelectFormatterUsingSortedAcceptHeaders(
|
protected virtual IOutputFormatter SelectFormatterUsingSortedAcceptHeaders(
|
||||||
OutputFormatterWriteContext formatterContext,
|
OutputFormatterWriteContext formatterContext,
|
||||||
IEnumerable<IOutputFormatter> formatters,
|
IList<IOutputFormatter> formatters,
|
||||||
IEnumerable<MediaTypeHeaderValue> sortedAcceptHeaders)
|
IList<MediaTypeHeaderValue> sortedAcceptHeaders)
|
||||||
{
|
{
|
||||||
if (formatterContext == null)
|
if (formatterContext == null)
|
||||||
{
|
{
|
||||||
|
|
@ -364,8 +378,8 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
|
||||||
/// </returns>
|
/// </returns>
|
||||||
protected virtual IOutputFormatter SelectFormatterUsingAnyAcceptableContentType(
|
protected virtual IOutputFormatter SelectFormatterUsingAnyAcceptableContentType(
|
||||||
OutputFormatterWriteContext formatterContext,
|
OutputFormatterWriteContext formatterContext,
|
||||||
IEnumerable<IOutputFormatter> formatters,
|
IList<IOutputFormatter> formatters,
|
||||||
IEnumerable<MediaTypeHeaderValue> acceptableContentTypes)
|
IList<MediaTypeHeaderValue> acceptableContentTypes)
|
||||||
{
|
{
|
||||||
if (formatterContext == null)
|
if (formatterContext == null)
|
||||||
{
|
{
|
||||||
|
|
@ -397,60 +411,113 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<MediaTypeHeaderValue> GetSortedAcceptHeaderMediaTypes(
|
// There's no allocation-free way to sort an IList so we're going to have to live with the
|
||||||
OutputFormatterWriteContext formatterContext)
|
// copy + insertion sort.
|
||||||
|
private IList<MediaTypeHeaderValue> PrepareAcceptValues(IList<MediaTypeHeaderValue> values)
|
||||||
{
|
{
|
||||||
var request = formatterContext.HttpContext.Request;
|
if (values == null || values.Count == 0)
|
||||||
var incomingAcceptHeaderMediaTypes = request.GetTypedHeaders().Accept ?? new MediaTypeHeaderValue[] { };
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// By default we want to ignore considering accept headers for content negotiation when
|
// By default we want to ignore considering accept headers for content negotiation when
|
||||||
// they have a media type like */* in them. Browsers typically have these media types.
|
// they have a media type like */* in them. Browsers typically have these media types.
|
||||||
// In these cases we would want the first formatter in the list of output formatters to
|
// In these cases we would want the first formatter in the list of output formatters to
|
||||||
// write the response. This default behavior can be changed through options, so checking here.
|
// write the response. This default behavior can be changed through options, so checking here.
|
||||||
var respectAcceptHeader = true;
|
if (!RespectBrowserAcceptHeader)
|
||||||
if (RespectBrowserAcceptHeader == false
|
|
||||||
&& incomingAcceptHeaderMediaTypes.Any(mediaType => mediaType.MatchesAllTypes))
|
|
||||||
{
|
{
|
||||||
respectAcceptHeader = false;
|
for (var i = 0; i < values.Count; i++)
|
||||||
|
{
|
||||||
|
if (values[i].MatchesAllTypes)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var sortedAcceptHeaderMediaTypes = Enumerable.Empty<MediaTypeHeaderValue>();
|
// Degenerate case, we can avoid copying anything.
|
||||||
if (respectAcceptHeader)
|
if (values.Count == 1)
|
||||||
{
|
{
|
||||||
sortedAcceptHeaderMediaTypes = SortMediaTypeHeaderValues(incomingAcceptHeaderMediaTypes)
|
return values;
|
||||||
.Where(header => header.Quality != HeaderQuality.NoMatch);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortedAcceptHeaderMediaTypes;
|
var sortNeeded = false;
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < values.Count; i++)
|
||||||
|
{
|
||||||
|
var value = values[i];
|
||||||
|
if (value.Quality == HeaderQuality.NoMatch)
|
||||||
|
{
|
||||||
|
// Exclude this one
|
||||||
|
}
|
||||||
|
else if (value.Quality != null)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
sortNeeded = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sortNeeded)
|
||||||
|
{
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sorted = new List<MediaTypeHeaderValue>(count);
|
||||||
|
for (var i = 0; i < values.Count; i++)
|
||||||
|
{
|
||||||
|
var value = values[i];
|
||||||
|
if (value.Quality == HeaderQuality.NoMatch)
|
||||||
|
{
|
||||||
|
// Exclude this one
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var position = sorted.BinarySearch(value, MediaTypeHeaderValueComparer.QualityComparer);
|
||||||
|
if (position >= 0)
|
||||||
|
{
|
||||||
|
sorted.Insert(position + 1, value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sorted.Insert(~position, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want a descending sort, but BinarySearch does ascending
|
||||||
|
sorted.Reverse();
|
||||||
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ValidateContentTypes(IList<MediaTypeHeaderValue> contentTypes)
|
private void ValidateContentTypes(IList<MediaTypeHeaderValue> contentTypes)
|
||||||
{
|
{
|
||||||
var matchAllContentType = contentTypes?.FirstOrDefault(
|
if (contentTypes == null)
|
||||||
contentType => contentType.MatchesAllSubTypes || contentType.MatchesAllTypes);
|
|
||||||
if (matchAllContentType != null)
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
return;
|
||||||
Resources.FormatObjectResult_MatchAllContentType(
|
}
|
||||||
matchAllContentType,
|
|
||||||
nameof(ObjectResult.ContentTypes)));
|
for (var i = 0; i < contentTypes.Count; i++)
|
||||||
|
{
|
||||||
|
var contentType = contentTypes[i];
|
||||||
|
if (contentType.MatchesAllTypes || contentType.MatchesAllSubTypes)
|
||||||
|
{
|
||||||
|
var message = Resources.FormatObjectResult_MatchAllContentType(
|
||||||
|
contentType,
|
||||||
|
nameof(ObjectResult.ContentTypes));
|
||||||
|
throw new InvalidOperationException(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This can't be cached, because
|
// This can't be cached, because the BindingContext is per-request.
|
||||||
private IList<IOutputFormatter> GetDefaultFormatters()
|
private IList<IOutputFormatter> GetDefaultFormatters()
|
||||||
{
|
{
|
||||||
return BindingContext?.OutputFormatters ?? OptionsFormatters;
|
return BindingContext?.OutputFormatters ?? OptionsFormatters;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<MediaTypeHeaderValue> SortMediaTypeHeaderValues(
|
|
||||||
IEnumerable<MediaTypeHeaderValue> headerValues)
|
|
||||||
{
|
|
||||||
// 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(
|
|
||||||
headerValue => headerValue,
|
|
||||||
MediaTypeHeaderValueComparer.QualityComparer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -526,7 +526,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
|
||||||
public new IOutputFormatter SelectFormatter(
|
public new IOutputFormatter SelectFormatter(
|
||||||
OutputFormatterWriteContext formatterContext,
|
OutputFormatterWriteContext formatterContext,
|
||||||
IList<MediaTypeHeaderValue> contentTypes,
|
IList<MediaTypeHeaderValue> contentTypes,
|
||||||
IEnumerable<IOutputFormatter> formatters)
|
IList<IOutputFormatter> formatters)
|
||||||
{
|
{
|
||||||
return base.SelectFormatter(formatterContext, contentTypes, formatters);
|
return base.SelectFormatter(formatterContext, contentTypes, formatters);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue