ASP.NET Core Web API:ProducesAttribute未提前过滤不支持的请求
问题
我有一个ASP.NET Core Web API,支持生成XML和JSON响应。其中一个控制器允许返回两种格式,但其他端点只需要返回JSON。现在当发送携带Accept: application/xml的请求时,会收到HTTP 406状态码,但对应的动作方法代码仍然会执行,造成不必要的资源消耗。
环境配置
我使用Visual Studio中的ASP.NET Core Web API模板(.NET 8)完成配置。Program.cs中的代码如下:
var builder = WebApplication.CreateBuilder(args); // 向容器添加服务 builder.Services.AddControllers(options => { // 启用对不支持的媒体类型返回406错误 options.ReturnHttpNotAcceptable = true; }) // 为API启用XML支持 .AddXmlSerializerFormatters(); var app = builder.Build(); // 配置HTTP请求管道 app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
随后在自动生成的WeatherForecastController中添加了Produces特性,代码如下:
namespace TestApi.Controllers { [ApiController] [Route("[controller]")] [Produces(MediaTypeNames.Application.Json)] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private readonly ILogger<WeatherForecastController> _logger; public WeatherForecastController(ILogger<WeatherForecastController> logger) { _logger = logger; } [HttpGet] public IEnumerable<WeatherForecast> Get() { return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) .ToArray(); } } }
当我发送如下请求时:
GET {{TestApi_HostAddress}}/weatherforecast/ Accept: application/xml
虽然会收到错误,但在Get()方法中设置断点后发现,ASP.NET Core在识别到无可用格式化器之前,已经执行了该方法的代码。
请问是否有办法让ASP.NET在内容协商阶段就确定返回406,而非先执行API中的代码?
解决方案
要在内容协商阶段提前拦截不支持的Accept头部并返回406,避免执行动作方法,可以通过自定义资源过滤器实现:
步骤1:创建资源过滤器
这个过滤器会在动作方法执行前,检查请求的Accept头部是否与控制器/动作声明的Produces特性兼容:
public class ValidateAcceptHeaderFilter : IResourceFilter { public void OnResourceExecuting(ResourceExecutingContext context) { // 获取当前动作或控制器上的Produces特性 var producesAttributes = context.ActionDescriptor.EndpointMetadata.OfType<ProducesAttribute>(); if (!producesAttributes.Any()) return; var supportedMediaTypes = producesAttributes.SelectMany(p => p.ContentTypes).ToList(); var acceptHeaders = context.HttpContext.Request.Headers.Accept; // 未指定Accept头部时默认允许(可根据需求调整) if (!acceptHeaders.Any()) return; // 检查是否存在匹配的媒体类型 var hasMatch = acceptHeaders.Any(accept => supportedMediaTypes.Any(supported => new MediaTypeHeaderValue(supported).IsSubsetOf(new MediaTypeHeaderValue(accept)))); if (!hasMatch) { context.Result = new StatusCodeResult(StatusCodes.Status406NotAcceptable); } } public void OnResourceExecuted(ResourceExecutedContext context) { // 无需处理后续逻辑 } }
步骤2:注册过滤器
可以选择全局注册(对所有控制器生效),或者仅在特定控制器/动作上添加:
全局注册(Program.cs)
builder.Services.AddControllers(options => { options.ReturnHttpNotAcceptable = true; // 添加自定义过滤器 options.Filters.Add<ValidateAcceptHeaderFilter>(); }) .AddXmlSerializerFormatters();
仅在特定控制器上添加
[ApiController] [Route("[controller]")] [Produces(MediaTypeNames.Application.Json)] // 为当前控制器添加过滤器 [ServiceFilter(typeof(ValidateAcceptHeaderFilter))] public class WeatherForecastController : ControllerBase { // 现有代码不变 }
这样配置后,当请求的Accept头部不在控制器支持的媒体类型范围内时,ASP.NET Core会在动作方法执行前直接返回406状态码,避免不必要的资源消耗。
内容的提问来源于stack exchange,提问作者mrbask
相关产品推荐
相关产品推荐

