| File: ConsumesAttribute.cs | Web Access |
| Project: src\aspnetcore\src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj (Microsoft.AspNetCore.Mvc.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; using Resources = Microsoft.AspNetCore.Mvc.Core.Resources; namespace Microsoft.AspNetCore.Mvc; /// <summary> /// A filter that specifies the supported request content types. <see cref="ContentTypes"/> is used to select an /// action when there would otherwise be multiple matches. /// </summary> [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class ConsumesAttribute : Attribute, IResourceFilter, IConsumesActionConstraint, IApiRequestMetadataProvider, IAcceptsMetadata { /// <summary> /// The order for consumes attribute. /// </summary> /// <value>Defaults to 200</value> public static readonly int ConsumesActionConstraintOrder = 200; /// <summary> /// Creates a new instance of <see cref="ConsumesAttribute"/>. /// <param name="contentType">The request content type.</param> /// <param name="otherContentTypes">The additional list of allowed request content types.</param> /// </summary> public ConsumesAttribute(string contentType, params string[] otherContentTypes) { ArgumentNullException.ThrowIfNull(contentType); // We want to ensure that the given provided content types are valid values, so // we validate them using the semantics of MediaTypeHeaderValue. MediaTypeHeaderValue.Parse(contentType); for (var i = 0; i < otherContentTypes.Length; i++) { MediaTypeHeaderValue.Parse(otherContentTypes[i]); } ContentTypes = GetContentTypes(contentType, otherContentTypes); _contentTypes = GetAllContentTypes(contentType, otherContentTypes); } /// <summary> /// Creates a new instance of <see cref="ConsumesAttribute"/>. /// <param name="requestType">The type being read from the request.</param> /// <param name="contentType">The request content type.</param> /// <param name="otherContentTypes">The additional list of allowed request content types.</param> /// </summary> public ConsumesAttribute(Type requestType, string contentType, params string[] otherContentTypes) { ArgumentNullException.ThrowIfNull(contentType); // We want to ensure that the given provided content types are valid values, so // we validate them using the semantics of MediaTypeHeaderValue. MediaTypeHeaderValue.Parse(contentType); for (var i = 0; i < otherContentTypes.Length; i++) { MediaTypeHeaderValue.Parse(otherContentTypes[i]); } ContentTypes = GetContentTypes(contentType, otherContentTypes); _contentTypes = GetAllContentTypes(contentType, otherContentTypes); _requestType = requestType; } // The value used is a non default value so that it avoids getting mixed with other action constraints // with default order. /// <inheritdoc /> int IActionConstraint.Order => ConsumesActionConstraintOrder; /// <summary> /// Gets or sets the supported request content types. Used to select an action when there would otherwise be /// multiple matches. /// </summary> public MediaTypeCollection ContentTypes { get; set; } /// <summary> /// Gets or sets a value that determines if the request body is optional. /// This value is only used to specify if the request body is required in API explorer. /// </summary> public bool IsOptional { get; set; } readonly Type? _requestType; readonly List<string> _contentTypes = new(); Type? IAcceptsMetadata.RequestType => _requestType; IReadOnlyList<string> IAcceptsMetadata.ContentTypes => _contentTypes; /// <inheritdoc /> public void OnResourceExecuting(ResourceExecutingContext context) { ArgumentNullException.ThrowIfNull(context); // Only execute if the current filter is the one which is closest to the action. // Ignore all other filters. This is to ensure we have a overriding behavior. if (IsApplicable(context.ActionDescriptor)) { var requestContentType = context.HttpContext.Request.ContentType; // Confirm the request's content type is more specific than a media type this action supports e.g. OK // if client sent "text/plain" data and this action supports "text/*". // // Requests without a content type do not return a 415. It is a common pattern to place [Consumes] on // a controller and have GET actions if (!string.IsNullOrEmpty(requestContentType) && !IsSubsetOfAnyContentType(requestContentType)) { context.Result = new UnsupportedMediaTypeResult(); } } } private bool IsSubsetOfAnyContentType(string requestMediaType) { var parsedRequestMediaType = new MediaType(requestMediaType); for (var i = 0; i < ContentTypes.Count; i++) { var contentTypeMediaType = new MediaType(ContentTypes[i]); if (parsedRequestMediaType.IsSubsetOf(contentTypeMediaType)) { return true; } } return false; } /// <inheritdoc /> public void OnResourceExecuted(ResourceExecutedContext context) { ArgumentNullException.ThrowIfNull(context); } /// <inheritdoc /> public bool Accept(ActionConstraintContext context) { // If this constraint is not closest to the action, it will be skipped. if (!IsApplicable(context.CurrentCandidate.Action)) { // Since the constraint is to be skipped, returning true here // will let the current candidate ignore this constraint and will // be selected based on other constraints for this action. return true; } var requestContentType = context.RouteContext.HttpContext.Request.ContentType; // If the request content type is null we need to act like pass through. // In case there is a single candidate with a constraint it should be selected. // If there are multiple actions with consumes action constraints this should result in ambiguous exception // unless there is another action without a consumes constraint. if (string.IsNullOrEmpty(requestContentType)) { var isActionWithoutConsumeConstraintPresent = context.Candidates.Any( candidate => candidate.Constraints == null || !candidate.Constraints.Any(constraint => constraint is IConsumesActionConstraint)); return !isActionWithoutConsumeConstraintPresent; } // Confirm the request's content type is more specific than (a media type this action supports e.g. OK // if client sent "text/plain" data and this action supports "text/*". if (IsSubsetOfAnyContentType(requestContentType)) { return true; } var firstCandidate = context.Candidates[0]; if (firstCandidate.Action != context.CurrentCandidate.Action) { // If the current candidate is not same as the first candidate, // we need not probe other candidates to see if they apply. // Only the first candidate is allowed to probe other candidates and based on the result select itself. return false; } // Run the matching logic for all IConsumesActionConstraints we can find, and see what matches. // 1). If we have a unique best match, then only that constraint should return true. // 2). If we have multiple matches, then all constraints that match will return true // , resulting in ambiguity(maybe). // 3). If we have no matches, then we choose the first constraint to return true.It will later return a 415 foreach (var candidate in context.Candidates) { if (candidate.Action == firstCandidate.Action) { continue; } var tempContext = new ActionConstraintContext() { Candidates = context.Candidates, RouteContext = context.RouteContext, CurrentCandidate = candidate }; if (candidate.Constraints == null || candidate.Constraints.Count == 0 || candidate.Constraints.Any(constraint => constraint is IConsumesActionConstraint && constraint.Accept(tempContext))) { // There is someone later in the chain which can handle the request. // end the process here. return false; } } // There is no one later in the chain that can handle this content type return a false positive so that // later we can detect and return a 415. return true; } private bool IsApplicable(ActionDescriptor actionDescriptor) { // If there are multiple IConsumeActionConstraints which are defined at the class and // at the action level, the one closest to the action overrides the others. To ensure this // we take advantage of the fact that ConsumesAttribute is both an IActionFilter and an // IConsumeActionConstraint. Since FilterDescriptor collection is ordered (the last filter is the one // closest to the action), we apply this constraint only if there is no IConsumeActionConstraint after this. return actionDescriptor.FilterDescriptors.Last( filter => filter.Filter is IConsumesActionConstraint).Filter == this; } private static MediaTypeCollection GetContentTypes(string firstArg, string[] args) { var completeArgs = new List<string>(args.Length + 1); completeArgs.Add(firstArg); completeArgs.AddRange(args); var contentTypes = new MediaTypeCollection(); foreach (var arg in completeArgs) { var mediaType = new MediaType(arg); if (mediaType.MatchesAllSubTypes || mediaType.MatchesAllTypes) { throw new InvalidOperationException( Resources.FormatMatchAllContentTypeIsNotAllowed(arg)); } contentTypes.Add(arg); } return contentTypes; } private static List<string> GetAllContentTypes(string contentType, string[] additionalContentTypes) { var allContentTypes = new List<string>() { contentType }; allContentTypes.AddRange(additionalContentTypes); return allContentTypes; } /// <inheritdoc /> public void SetContentTypes(MediaTypeCollection contentTypes) { contentTypes.Clear(); foreach (var contentType in ContentTypes) { contentTypes.Add(contentType); } } }