|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable
using System.Collections.ObjectModel;
using System.Globalization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.Infrastructure;
/// <summary>
/// The default implementation of <see cref="OutputFormatterSelector"/>.
/// </summary>
public partial class DefaultOutputFormatterSelector : OutputFormatterSelector
{
private static readonly Comparison<MediaTypeSegmentWithQuality> _sortFunction = (left, right) =>
{
return left.Quality > right.Quality ? -1 : (left.Quality == right.Quality ? 0 : 1);
};
private readonly ILogger _logger;
private readonly IList<IOutputFormatter> _formatters;
private readonly bool _respectBrowserAcceptHeader;
private readonly bool _returnHttpNotAcceptable;
/// <summary>
/// Initializes a new instance of <see cref="DefaultOutputFormatterSelector"/>
/// </summary>
/// <param name="options">Used to access <see cref="MvcOptions"/>.</param>
/// <param name="loggerFactory">The logger factory.</param>
public DefaultOutputFormatterSelector(IOptions<MvcOptions> options, ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(loggerFactory);
_logger = loggerFactory.CreateLogger<DefaultOutputFormatterSelector>();
_formatters = new ReadOnlyCollection<IOutputFormatter>(options.Value.OutputFormatters);
_respectBrowserAcceptHeader = options.Value.RespectBrowserAcceptHeader;
_returnHttpNotAcceptable = options.Value.ReturnHttpNotAcceptable;
}
/// <inheritdoc/>
public override IOutputFormatter? SelectFormatter(OutputFormatterCanWriteContext context, IList<IOutputFormatter> formatters, MediaTypeCollection contentTypes)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(formatters);
ArgumentNullException.ThrowIfNull(contentTypes);
ValidateContentTypes(contentTypes);
if (formatters.Count == 0)
{
formatters = _formatters;
if (formatters.Count == 0)
{
throw new InvalidOperationException(Resources.FormatOutputFormattersAreRequired(
typeof(MvcOptions).FullName,
nameof(MvcOptions.OutputFormatters),
typeof(IOutputFormatter).FullName));
}
}
Log.RegisteredOutputFormatters(_logger, formatters);
var request = context.HttpContext.Request;
var acceptableMediaTypes = GetAcceptableMediaTypes(request);
var selectFormatterWithoutRegardingAcceptHeader = false;
IOutputFormatter? selectedFormatter = null;
if (acceptableMediaTypes.Count == 0)
{
// There is either no Accept header value, or it contained */* and we
// are not currently respecting the 'browser accept header'.
Log.NoAcceptForNegotiation(_logger);
selectFormatterWithoutRegardingAcceptHeader = true;
}
else
{
if (contentTypes.Count == 0)
{
Log.SelectingOutputFormatterUsingAcceptHeader(_logger, acceptableMediaTypes);
// Use whatever formatter can meet the client's request
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
context,
formatters,
acceptableMediaTypes);
}
else
{
Log.SelectingOutputFormatterUsingAcceptHeaderAndExplicitContentTypes(_logger, acceptableMediaTypes, contentTypes);
// Verify that a content type from the context is compatible with the client's request
selectedFormatter = SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
context,
formatters,
acceptableMediaTypes,
contentTypes);
}
if (selectedFormatter == null)
{
Log.NoFormatterFromNegotiation(_logger, acceptableMediaTypes);
if (!_returnHttpNotAcceptable)
{
selectFormatterWithoutRegardingAcceptHeader = true;
}
}
}
if (selectFormatterWithoutRegardingAcceptHeader)
{
if (contentTypes.Count == 0)
{
Log.SelectingOutputFormatterWithoutUsingContentTypes(_logger);
selectedFormatter = SelectFormatterNotUsingContentType(
context,
formatters);
}
else
{
Log.SelectingOutputFormatterUsingContentTypes(_logger, contentTypes);
selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(
context,
formatters,
contentTypes);
}
}
if (selectedFormatter != null)
{
Log.FormatterSelected(_logger, selectedFormatter, context);
}
return selectedFormatter;
}
private List<MediaTypeSegmentWithQuality> GetAcceptableMediaTypes(HttpRequest request)
{
var result = new List<MediaTypeSegmentWithQuality>();
AcceptHeaderParser.ParseAcceptHeader(request.Headers.Accept, result);
for (var i = 0; i < result.Count; i++)
{
var mediaType = new MediaType(result[i].MediaType);
if (!_respectBrowserAcceptHeader && mediaType.MatchesAllSubTypes && mediaType.MatchesAllTypes)
{
result.Clear();
return result;
}
}
result.Sort(_sortFunction);
return result;
}
private IOutputFormatter? SelectFormatterNotUsingContentType(
OutputFormatterCanWriteContext formatterContext,
IList<IOutputFormatter> formatters)
{
Log.SelectFirstCanWriteFormatter(_logger);
foreach (var formatter in formatters)
{
formatterContext.ContentType = new StringSegment();
formatterContext.ContentTypeIsServerDefined = false;
if (formatter.CanWriteResult(formatterContext))
{
return formatter;
}
}
return null;
}
private static IOutputFormatter? SelectFormatterUsingSortedAcceptHeaders(
OutputFormatterCanWriteContext formatterContext,
IList<IOutputFormatter> formatters,
IList<MediaTypeSegmentWithQuality> sortedAcceptHeaders)
{
for (var i = 0; i < sortedAcceptHeaders.Count; i++)
{
var mediaType = sortedAcceptHeaders[i];
formatterContext.ContentType = mediaType.MediaType;
formatterContext.ContentTypeIsServerDefined = false;
for (var j = 0; j < formatters.Count; j++)
{
var formatter = formatters[j];
if (formatter.CanWriteResult(formatterContext))
{
return formatter;
}
}
}
return null;
}
private static IOutputFormatter? SelectFormatterUsingAnyAcceptableContentType(
OutputFormatterCanWriteContext formatterContext,
IList<IOutputFormatter> formatters,
MediaTypeCollection acceptableContentTypes)
{
foreach (var formatter in formatters)
{
foreach (var contentType in acceptableContentTypes)
{
formatterContext.ContentType = new StringSegment(contentType);
formatterContext.ContentTypeIsServerDefined = true;
if (formatter.CanWriteResult(formatterContext))
{
return formatter;
}
}
}
return null;
}
private static IOutputFormatter? SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
OutputFormatterCanWriteContext formatterContext,
IList<IOutputFormatter> formatters,
IList<MediaTypeSegmentWithQuality> sortedAcceptableContentTypes,
MediaTypeCollection possibleOutputContentTypes)
{
for (var i = 0; i < sortedAcceptableContentTypes.Count; i++)
{
var acceptableContentType = new MediaType(sortedAcceptableContentTypes[i].MediaType);
for (var j = 0; j < possibleOutputContentTypes.Count; j++)
{
var candidateContentType = new MediaType(possibleOutputContentTypes[j]);
if (candidateContentType.IsSubsetOf(acceptableContentType))
{
for (var k = 0; k < formatters.Count; k++)
{
var formatter = formatters[k];
formatterContext.ContentType = new StringSegment(possibleOutputContentTypes[j]);
formatterContext.ContentTypeIsServerDefined = true;
if (formatter.CanWriteResult(formatterContext))
{
return formatter;
}
}
}
}
}
return null;
}
private static void ValidateContentTypes(MediaTypeCollection contentTypes)
{
for (var i = 0; i < contentTypes.Count; i++)
{
var contentType = contentTypes[i];
var parsedContentType = new MediaType(contentType);
if (parsedContentType.HasWildcard)
{
var message = Resources.FormatObjectResult_MatchAllContentType(
contentType,
nameof(ObjectResult.ContentTypes));
throw new InvalidOperationException(message);
}
}
}
private static partial class Log
{
public static void FormatterSelected(
ILogger logger,
IOutputFormatter outputFormatter,
OutputFormatterCanWriteContext context)
{
if (logger.IsEnabled(LogLevel.Debug))
{
var contentType = Convert.ToString(context.ContentType, CultureInfo.InvariantCulture);
FormatterSelected(logger, outputFormatter, contentType);
}
}
[LoggerMessage(2, LogLevel.Debug, "Selected output formatter '{OutputFormatter}' and content type '{ContentType}' to write the response.", EventName = "FormatterSelected", SkipEnabledCheck = true)]
public static partial void FormatterSelected(ILogger logger, IOutputFormatter outputFormatter, string? contentType);
[LoggerMessage(4, LogLevel.Debug, "No information found on request to perform content negotiation.", EventName = "NoAcceptForNegotiation")]
public static partial void NoAcceptForNegotiation(ILogger logger);
[LoggerMessage(5, LogLevel.Debug, "Could not find an output formatter based on content negotiation. Accepted types were ({AcceptTypes})", EventName = "NoFormatterFromNegotiation")]
public static partial void NoFormatterFromNegotiation(ILogger logger, IList<MediaTypeSegmentWithQuality> acceptTypes);
[LoggerMessage(6, LogLevel.Debug, "Attempting to select an output formatter based on Accept header '{AcceptHeader}'.", EventName = "SelectingOutputFormatterUsingAcceptHeader")]
public static partial void SelectingOutputFormatterUsingAcceptHeader(ILogger logger, IEnumerable<MediaTypeSegmentWithQuality> acceptHeader);
[LoggerMessage(7, LogLevel.Debug, "Attempting to select an output formatter based on Accept header '{AcceptHeader}' and explicitly specified content types '{ExplicitContentTypes}'. The content types in the accept header must be a subset of the explicitly set content types.", EventName = "SelectingOutputFormatterUsingAcceptHeaderAndExplicitContentTypes")]
public static partial void SelectingOutputFormatterUsingAcceptHeaderAndExplicitContentTypes(ILogger logger, IEnumerable<MediaTypeSegmentWithQuality> acceptHeader, MediaTypeCollection explicitContentTypes);
[LoggerMessage(8, LogLevel.Debug, "Attempting to select an output formatter without using a content type as no explicit content types were specified for the response.", EventName = "SelectingOutputFormatterWithoutUsingContentTypes")]
public static partial void SelectingOutputFormatterWithoutUsingContentTypes(ILogger logger);
[LoggerMessage(9, LogLevel.Debug, "Attempting to select the first output formatter in the output formatters list which supports a content type from the explicitly specified content types '{ExplicitContentTypes}'.", EventName = "SelectingOutputFormatterUsingContentTypes")]
public static partial void SelectingOutputFormatterUsingContentTypes(ILogger logger, MediaTypeCollection explicitContentTypes);
[LoggerMessage(10, LogLevel.Debug, "Attempting to select the first formatter in the output formatters list which can write the result.", EventName = "SelectingFirstCanWriteFormatter")]
public static partial void SelectFirstCanWriteFormatter(ILogger logger);
[LoggerMessage(11, LogLevel.Debug, "List of registered output formatters, in the following order: {OutputFormatters}", EventName = "RegisteredOutputFormatters")]
public static partial void RegisteredOutputFormatters(ILogger logger, IEnumerable<IOutputFormatter> outputFormatters);
}
}
|