【问题标题】:Unable to get ASP.NET web api to consume an unspecified content type无法让 ASP.NET Web api 使用未指定的内容类型
【发布时间】:2021-04-25 01:17:06
【问题描述】:

我正在尝试让 Web api(控制器)在客户端未指定接受/内容类型标头的情况下使用 HTTP 发布请求。

我在控制器中有 2 种方法 - 一种使用 text/plain(旧版),另一种使用 application/json(新)。问题是我们有期望基于文本的请求/响应的旧环境,但他们没有指定请求的内容类型。由于这些环境在一段时间内不会升级,我不确定如何强制它将 text/plain 作为默认媒体类型处理。

当我在未指定内容类型的情况下发出请求时,我收到“不支持的媒体类型”错误。

Consums 属性不支持空媒体类型。

【问题讨论】:

    标签: c# asp.net-core model-view-controller mime-types content-type


    【解决方案1】:

    我能够通过使用 MVC 代码作为模板创建自定义“Consume”属性并创建自己的 InputFormatter 来解决这种情况。

    属性:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.AspNetCore.Mvc;
    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;
    
    
    
    
    namespace MyNamespace
    {
    
    
        internal interface IConsumesNullConstraint : IActionConstraint
        { }
        /// <summary>
        /// A filter that specifies the supported request content types. <see cref="ContentTypes"/>
        /// </summary>
        [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
        public class ConsumesNullOrTextAttribute :
                Attribute,
                IResourceFilter,
                IActionConstraint,
                IConsumesNullConstraint,
                IApiRequestMetadataProvider
        {
            /// <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="ConsumesNullOrTextAttribute"/>.
            /// </summary>
            public ConsumesNullOrTextAttribute()
            {
    
    
                ContentTypes = GetContentTypes("text/plain");
            }
    
            // 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>
            /// Media types to support in addition to null
            /// </summary>
            public MediaTypeCollection ContentTypes { get; set; }
    
            /// <summary>
            /// Validates it is an expected media type we want to support
            /// </summary>
            /// <param name="context"></param>
            public void OnResourceExecuting(ResourceExecutingContext context)
            {
                if (context == null)
                {
                    throw new ArgumentNullException(nameof(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;
    
    
                    // If we got a content type - make sure it is a supported type
                    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)
            {
                if (context == null)
                {
                    throw new ArgumentNullException(nameof(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 null, we are okay
                if (string.IsNullOrEmpty(requestContentType))
                {
                    return true;
                }
    
                // 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 IConsumesNullConstraint &&
                                                                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 IConsumesNullConstraint).Filter == this;
            }
    
            private MediaTypeCollection GetContentTypes(string firstArg, string[] args = null)
            {
                var completeArgs = new List<string>((args?.Length ?? 0) + 1);
                completeArgs.Add(firstArg);
                if (args != null)
                    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(
                          $"Unsupported media type {arg}");
                    }
    
                    contentTypes.Add(arg);
                }
    
                return contentTypes;
            }
    
            /// <inheritdoc />
            public void SetContentTypes(MediaTypeCollection contentTypes)
            {
                contentTypes.Clear();
                foreach (var contentType in ContentTypes)
                {
                    contentTypes.Add(contentType);
                }
            }
        }
    }
    

    和格式化程序:

    using Microsoft.AspNetCore.Mvc.Formatters;
    using Microsoft.Net.Http.Headers;
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Threading.Tasks;
    
    namespace MyNameSpace
    {
        /// <summary>
        /// Formatter that allows content of type text/plain 
        /// or no content type to be parsed to raw data. Allows for a single input parameter
        /// in the form of:
        /// 
        /// public string RawString([FromBody] string data)
        /// public byte[] RawData([FromBody] byte[] data)
        /// </summary>
        public class RawRequestBodyFormatter : InputFormatter
        {
            public RawRequestBodyFormatter()
            {
                SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
                SupportedMediaTypes.Add((string)null);
            }
    
    
            /// <summary>
            /// Allow text/plain, application/octet-stream and no content type to
            /// be processed
            /// </summary>
            /// <param name="context"></param>
            /// <returns></returns>
            public override Boolean CanRead(InputFormatterContext context)
            {
                if (context == null) throw new ArgumentNullException(nameof(context));
    
                var contentType = context.HttpContext.Request.ContentType;
                if (string.IsNullOrEmpty(contentType) || contentType == "text/plain")
                    return true;
    
                return false;
            }
    
            /// <summary>
            /// Handle text/plain or no content type for string results
            /// Handle application/octet-stream for byte[] results
            /// </summary>
            /// <param name="context"></param>
            /// <returns></returns>
            public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
            {
                var request = context.HttpContext.Request;
                var contentType = context.HttpContext.Request.ContentType;
    
                // process stream as text
                if (string.IsNullOrEmpty(contentType) || contentType == "text/plain")
                {
                    using (var reader = new StreamReader(request.Body))
                    {
                        var content = await reader.ReadToEndAsync();
                        return await InputFormatterResult.SuccessAsync(content);
                    }
                }
    
                return await InputFormatterResult.FailureAsync();
            }
        }
    }
    

    在您的配置服务方法中,您需要添加以下内容:

    public void ConfigureServices(IServiceCollection services)
    {
        ...
        services.AddControllers(o => o.InputFormatters.Insert(o.InputFormatters.Count, new RawRequestBodyFormatter()));
        ...
    }
    

    最后在你的控制器方法上用新属性和一个 [FromBody] 参数来装饰它:

    [HttpPost]
    [Route("api/MyAPI")]
    [ConsumesNullOrText()]
    public async Task<string> MyApiHandler([FromBody] string content)
    {
        ...
    }
        
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-11
      • 2016-03-10
      • 2015-10-15
      • 2018-08-16
      • 2016-02-07
      • 1970-01-01
      相关资源
      最近更新 更多