Search code examples
c#asp.net-core-webapicontent-negotiationhttp-status-code-406

ProducesAttribute does not filter requests in ASP.NET Core Web API before code is executed


Problem I have an ASP.NET Core Web API that can produce XML and JSON responses. For one of my controllers, I allow producing both XML and JSON. However, for all other endpoints, I only want to produce JSON. I currently receive an HTTP status code 406 when a request is sent with Accept: application/xml. However, the code belonging to the action is executed, consuming unnecessary resources.

Set-up I managed to set this up quite easily using the ASP.NET Core Web API template in Visual Studio (.NET 8). In the Program.cs I have:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers(options =>
    {
        // This line enables returning 406 errors for unsupported media types.
        options.ReturnHttpNotAcceptable = true;
    })
    // This line enables XML support for the APIs.
    .AddXmlSerializerFormatters();

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Then in the WeatherForecastController that is automatically generated I added a Produces tag like this:

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();
        }
    }
}

Now when I make a request like this:

GET {{TestApi_HostAddress}}/weatherforecast/
Accept: application/xml

I do get an error, but putting a breakpoint in the Get() method shows that the code is executed before ASP.NET Core realizes there is no available formatter.

Is there a way to ensure ASP.NET knows during the content negotiation phase that a 406 can be given instead of running the code in my API first?

Documentation https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-8.0#specify-a-format-2


Solution

  • You can implement this feature by using custom ProducesJsonOnlyFilter. It works fine in my side, you can try it.

    ProducesJsonOnlyFilter.cs

    using Microsoft.AspNetCore.Mvc.Filters;
    using Microsoft.AspNetCore.Mvc;
    
    namespace _79255649
    {
        public class ProducesJsonOnlyFilter : IActionFilter
        {
            public void OnActionExecuting(ActionExecutingContext context)
            {
                var acceptHeader = context.HttpContext.Request.Headers["Accept"].ToString();
                if (!string.IsNullOrEmpty(acceptHeader) && !acceptHeader.Contains("application/json", StringComparison.OrdinalIgnoreCase))
                {
                    context.Result = new StatusCodeResult(StatusCodes.Status406NotAcceptable);
                }
            }
    
            public void OnActionExecuted(ActionExecutedContext context) { }
        }
    
    }
    

    Register the filter

    using _79255649;
    
    var builder = WebApplication.CreateBuilder(args);
    
    
    //builder.Services.AddControllers();
    builder.Services.AddControllers(options =>
    {
        // This line enables returning 406 errors for unsupported media types.
        options.ReturnHttpNotAcceptable = true;
    })
    // This line enables XML support for the APIs.
    .AddXmlSerializerFormatters();
    // Register ProducesJsonOnlyFilter
    // Add below line
    builder.Services.AddScoped<ProducesJsonOnlyFilter>();
    // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    

    Apply it in controller

    using Microsoft.AspNetCore.Mvc;
    using System.Net.Mime;
    
    namespace _79255649.Controllers
    {
        [ApiController]
        [Route("[controller]")]
        //[Produces(MediaTypeNames.Application.Json)]
        // Use it like below
        [ServiceFilter(typeof(ProducesJsonOnlyFilter))]
        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(Name = "GetWeatherForecast")]
            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();
            }
        }
    }