Content Negotiation in Web API

By FoxLearn 1/17/2025 8:26:02 AM   26
Content negotiation is a process that enables a client to request a specific representation of a resource, such as JSON, XML, or plain text, when multiple formats are available.

Although this feature has been a part of HTTP for a long time, it is often underused in API development.

In this example, we will build a simple product catalog API and demonstrate how content negotiation works in ASP.NET Core Web API. This will include returning responses in different formats (JSON, XML) based on the client’s preferences, and also how to restrict and extend the available formats using custom formatters.

How Content Negotiation Works

Content negotiation occurs when a client sends an Accept header in the HTTP request, specifying the desired media type for the response.

By default, ASP.NET Core Web API returns responses in JSON format, but you can configure it to return other formats as well, such as XML or plain text.

ASP.NET Core supports the following media types by default:

  • application/json
  • text/json
  • text/plain

Let's begin by creating a simple product catalog with a Product model and a ProductCatalog controller.

Define the Product Model

We’ll define a simple Product model to represent products in the catalog.

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Description { get; set; }
    public bool InStock { get; set; }
}

Create the ProductCatalog Controller

Next, we create the ProductCatalogController that returns a list of products.

[Route("api/products")]
public class ProductCatalogController : Controller
{
    public IActionResult Get()
    {
        var products = new List<Product>
        {
            new Product { Name = "Laptop", Price = 999.99, Description = "High-performance laptop", InStock = true },
            new Product { Name = "Smartphone", Price = 599.99, Description = "Latest model smartphone", InStock = true },
            new Product { Name = "Headphones", Price = 199.99, Description = "Noise-cancelling headphones", InStock = false }
        };
        return Ok(products);
    }
}

This will use the Ok() method to return the list of products as the response body.

Default JSON Response

When you run the application and request the /api/products endpoint, the server will return the products in JSON format by default:

[
  {
    "name": "Laptop",
    "price": 999.99,
    "description": "High-performance laptop",
    "inStock": true
  },
  {
    "name": "Smartphone",
    "price": 599.99,
    "description": "Latest model smartphone",
    "inStock": true
  },
  {
    "name": "Headphones",
    "price": 199.99,
    "description": "Noise-cancelling headphones",
    "inStock": false
  }
]

Adding XML Support

If we want to return XML instead of JSON, we need to configure the Web API to handle the Accept header properly. We can add XML formatters by modifying the configuration in the Program.cs or Startup.cs file.

Modify the configuration to respect the browser's Accept header and include XML formatters:

builder.Services.AddControllers(options =>
{
    options.RespectBrowserAcceptHeader = true;
}).AddXmlSerializerFormatters();

Now, if the client sends an Accept: application/xml header, the server will respond with an XML representation of the products:

<ArrayOfProduct xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/ProductCatalog">
    <Product>
        <Description>High-performance laptop</Description>
        <InStock>true</InStock>
        <Name>Laptop</Name>
        <Price>999.99</Price>
    </Product>
    <Product>
        <Description>Latest model smartphone</Description>
        <InStock>true</InStock>
        <Name>Smartphone</Name>
        <Price>599.99</Price>
    </Product>
    <Product>
        <Description>Noise-cancelling headphones</Description>
        <InStock>false</InStock>
        <Name>Headphones</Name>
        <Price>199.99</Price>
    </Product>
</ArrayOfProduct>

Restricting Media Types

You might want to restrict the media types your API can handle.

For example, if a client requests a media type that your API doesn't support, you can return a 406 Not Acceptable status.

builder.Services.AddControllers(options =>
{
    options.RespectBrowserAcceptHeader = true;
    options.ReturnHttpNotAcceptable = true;
}).AddXmlSerializerFormatters();

This is done by adding the ReturnHttpNotAcceptable option in the configuration.

Now, if the client requests a media type that is unsupported (like text/html), the API will return a 406 Not Acceptable status.

Custom Formatters for Specialized Responses

Sometimes, you may need to create a custom response format, such as CSV. ASP.NET Core allows you to create custom output formatters for handling non-standard formats.

Let’s implement a custom CsvOutputFormatter for our product catalog.

public class CsvOutputFormatter : TextOutputFormatter
{
    public CsvOutputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/csv"));
        SupportedEncodings.Add(Encoding.UTF8);
    }

    protected override bool CanWriteType(Type? type)
        => typeof(IEnumerable<Product>).IsAssignableFrom(type);

    public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        var response = context.HttpContext.Response;
        var buffer = new StringBuilder();
        
        if (context.Object is IEnumerable<Product> products)
        {
            foreach (var product in products)
            {
                buffer.AppendLine($"{product.Name}, {product.Price}, {product.Description}, {product.InStock}");
            }
        }

        await response.WriteAsync(buffer.ToString(), selectedEncoding);
    }
}

To use this custom formatter, we need to add it to the list of output formatters in the configuration:

builder.Services.AddControllers(options =>
{
    options.RespectBrowserAcceptHeader = true;
    options.ReturnHttpNotAcceptable = true;
}).AddXmlSerializerFormatters()
  .AddMvcOptions(options => options.OutputFormatters.Add(new CsvOutputFormatter()));

Now, if a client requests Accept: application/csv, the server will respond with the product data in CSV format:

Laptop, 999.99, High-performance laptop, True
Smartphone, 599.99, Latest model smartphone, True
Headphones, 199.99, Noise-cancelling headphones, False

In this example, we demonstrated how content negotiation works in ASP.NET Core Web API using a product catalog. We covered:

  • Returning the default JSON response
  • Configuring the API to return XML based on the Accept header
  • Restricting unsupported media types and returning a 406 Not Acceptable status
  • Implementing a custom CSV output formatter for specialized content

By utilizing content negotiation, you can make your API more flexible and adapt to different client needs, providing responses in multiple formats while maintaining control over the supported media types.