Can I use Content Negotiation to return a View to browers and JSON to API calls in ASP.NET Core?
asp.net-core, asp.net-mvc, c#
Solution
I think this is a reasonable use case as it would simplify creating APIs that return both HTML and JSON/XML/etc from a single controller. This would allow for progressive enhancement, as well as several other benefits, though it might not work well in cases where the API and Mvc behavior needs to be drastically different.
I have done this with a custom filter, with some caveats below:
public class ViewIfAcceptHtmlAttribute : Attribute, IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.HttpContext.Request.Headers["Accept"].ToString().Contains("text/html"))
{
var originalResult = context.Result as ObjectResult;
var controller = context.Controller as Controller;
if(originalResult != null && controller != null)
{
var model = originalResult.Value;
var newResult = controller.View(model);
newResult.StatusCode = originalResult.StatusCode;
context.Result = newResult;
}
}
}
public void OnActionExecuting(ActionExecutingContext context)
{
}
}
which can be added to a controller or action:
[ViewIfAcceptHtml]
[Route("/foo/")]
public IActionResult Get(){
return Ok(new Foo());
}
or registered globally in Startup.cs
services.AddMvc(x=>
{
x.Filters.Add(new ViewIfAcceptHtmlAttribute());
});
This works for my use case and accomplishes the goal of supporting text/html and application/json from the same controller. I suspect isn't the "best" approach as it side-steps the custom formatters. Ideally (in my mind), this code would just be another Formatter like Xml and Json, but that outputs Html using the View rendering engine. That interface is a little more involved, though, and this was the simplest thing that works for now.
Problem
I've got a pretty basic controller method that returns a list of Customers. I want it to return the List View when a user browses to it, and return JSON to requests that have `application/json` in the Accept header. Is that possible in ASP.NET Core MVC 1.0? I've tried this: ``` [HttpGet("")] public async Task<IActionResult> List(int page = 1, int count = 20) { var customers = await _customerService.GetCustomers(page, count); return Ok(customers.Select(c => new { c.Id, c.Name })); } ``` But that returns JSON by default, even if it's not in the Accept list. If I hit "/customers" in my browser, I get the JSON output, not my view. I thought I might need to write an OutputFormatter that handled `text/html`, but I can't figure out how I can call the `View()` method from an `OutputFormatter`, since those methods are on `Controller`, and I'd need to know the name of the View I wanted to render. Is there a method or property I can call to check if MVC will be able to find an `OutputFormatter` to render? Something like the following: ``` [HttpGet("")] public async Task<IActionResult> List(int page = 1, int count = 20) { var customers = await _customerService.GetCustomers(page, count); if(Response.WillUseContentNegotiation) { return Ok(customers.Select(c => new { c.Id, c.Name })); } else { return View(customers.Select(c => new { c.Id, c.Name })); } } ```