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