|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Formatters;
/// <summary>
/// A filter that will use the format value in the route data or query string to set the content type on an
/// <see cref="ObjectResult"/> returned from an action.
/// </summary>
public partial class FormatFilter : IFormatFilter, IResourceFilter, IResultFilter
{
private readonly MvcOptions _options;
private readonly ILogger _logger;
/// <summary>
/// Initializes an instance of <see cref="FormatFilter"/>.
/// </summary>
/// <param name="options">The <see cref="IOptions{MvcOptions}"/></param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public FormatFilter(IOptions<MvcOptions> options, ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(loggerFactory);
_options = options.Value;
_logger = loggerFactory.CreateLogger(GetType());
}
/// <inheritdoc />
public virtual string? GetFormat(ActionContext context)
{
if (context.RouteData.Values.TryGetValue("format", out var obj))
{
// null and string.Empty are equivalent for route values.
var routeValue = Convert.ToString(obj, CultureInfo.InvariantCulture);
return string.IsNullOrEmpty(routeValue) ? null : routeValue;
}
var query = context.HttpContext.Request.Query["format"];
if (query.Count > 0)
{
return query.ToString();
}
return null;
}
/// <summary>
/// As a <see cref="IResourceFilter"/>, this filter looks at the request and rejects it before going ahead if
/// 1. The format in the request does not match any format in the map.
/// 2. If there is a conflicting producesFilter.
/// </summary>
/// <param name="context">The <see cref="ResourceExecutingContext"/>.</param>
public void OnResourceExecuting(ResourceExecutingContext context)
{
ArgumentNullException.ThrowIfNull(context);
var format = GetFormat(context);
if (format == null)
{
// no format specified by user, so the filter is muted
return;
}
var contentType = _options.FormatterMappings.GetMediaTypeMappingForFormat(format);
if (contentType == null)
{
Log.UnsupportedFormatFilterContentType(_logger, format);
// no contentType exists for the format, return 404
context.Result = new NotFoundResult();
return;
}
// Determine media types this action supports.
var responseTypeFilters = context.Filters.OfType<IApiResponseMetadataProvider>();
var supportedMediaTypes = new MediaTypeCollection();
foreach (var filter in responseTypeFilters)
{
filter.SetContentTypes(supportedMediaTypes);
}
// Check if support is adequate for requested media type.
if (supportedMediaTypes.Count == 0)
{
Log.ActionDoesNotExplicitlySpecifyContentTypes(_logger);
return;
}
// We need to check if the action can generate the content type the user asked for. That is, treat the
// request's format and IApiResponseMetadataProvider-provided content types similarly to an Accept
// header and an output formatter's SupportedMediaTypes: Confirm action supports a more specific media
// type than requested e.g. OK if "text/*" requested and action supports "text/plain".
if (!IsSuperSetOfAnySupportedMediaType(contentType, supportedMediaTypes))
{
Log.ActionDoesNotSupportFormatFilterContentType(_logger, contentType, supportedMediaTypes);
context.Result = new NotFoundResult();
}
}
private static bool IsSuperSetOfAnySupportedMediaType(string contentType, MediaTypeCollection supportedMediaTypes)
{
var parsedContentType = new MediaType(contentType);
for (var i = 0; i < supportedMediaTypes.Count; i++)
{
var supportedMediaType = new MediaType(supportedMediaTypes[i]);
if (supportedMediaType.IsSubsetOf(parsedContentType))
{
return true;
}
}
return false;
}
/// <inheritdoc />
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
/// <summary>
/// Sets a Content Type on an <see cref="ObjectResult" /> using a format value from the request.
/// </summary>
/// <param name="context">The <see cref="ResultExecutingContext"/>.</param>
public void OnResultExecuting(ResultExecutingContext context)
{
ArgumentNullException.ThrowIfNull(context);
var format = GetFormat(context);
if (format == null)
{
// no format specified by user, so the filter is muted
return;
}
if (context.Result is not ObjectResult objectResult)
{
return;
}
// If the action sets a single content type, then it takes precedence over the user
// supplied content type based on format mapping.
if (objectResult.ContentTypes.Count == 1 ||
!string.IsNullOrEmpty(context.HttpContext.Response.ContentType))
{
Log.CannotApplyFormatFilterContentType(_logger, format);
return;
}
var contentType = _options.FormatterMappings.GetMediaTypeMappingForFormat(format);
objectResult.ContentTypes.Clear();
if (!string.IsNullOrEmpty(contentType))
{
objectResult.ContentTypes.Add(contentType);
}
}
/// <inheritdoc />
public void OnResultExecuted(ResultExecutedContext context)
{
}
private static partial class Log
{
[LoggerMessage(1, LogLevel.Debug, "Could not find a media type for the format '{FormatFilterContentType}'.", EventName = "UnsupportedFormatFilterContentType")]
public static partial void UnsupportedFormatFilterContentType(ILogger logger, string formatFilterContentType);
[LoggerMessage(2, LogLevel.Debug, "Current action does not support the content type '{FormatFilterContentType}'. The supported content types are '{SupportedMediaTypes}'.", EventName = "ActionDoesNotSupportFormatFilterContentType")]
public static partial void ActionDoesNotSupportFormatFilterContentType(ILogger logger, string formatFilterContentType, MediaTypeCollection supportedMediaTypes);
[LoggerMessage(3, LogLevel.Debug, "Cannot apply content type '{FormatFilterContentType}' to the response as current action had explicitly set a preferred content type.", EventName = "CannotApplyFormatFilterContentType")]
public static partial void CannotApplyFormatFilterContentType(ILogger logger, string formatFilterContentType);
[LoggerMessage(5, LogLevel.Debug, "Current action does not explicitly specify any content types for the response.", EventName = "ActionDoesNotExplicitlySpecifyContentTypes")]
public static partial void ActionDoesNotExplicitlySpecifyContentTypes(ILogger logger);
}
}
|