In this article, we’ll examine how to implement content negotiation in an ASP.NET Core Minimal API. Since there are several ways to implement content negotiation in .NET APIs, we’ll briefly discuss the different approaches and tradeoffs for each.
Let’s focus on the ASP.NET Core Minimal API and then use the Carter library to make our lives easier.
Content Negotiation in Web API
Let’s start by briefly discussing what content negotiation is, and what our options are for implementing it in .NET.
What Is Content Negotiation?
Content negotiation (or “conneg” as it’s sometimes referred to) is the ability of a web server to “negotiate” the content requested by the client. The HTTP client (web browser, mobile application, or another client app making HTTP requests) would usually pass through an Accept
header, specifying the content it prefers, and ideally, the web server should return the response in that format.
We use the word “ideally” because, off the shelf, .NET APIs don’t support this out of the box. So, if this is an essential feature to us, we need to add some special support to our application, and as we’ll learn throughout this article, there are easy and hard ways to accomplish this.
Content Negotiation in ASP.NET Core Minimal APIs
As we know, ASP.NET Core Minimal APIs are designed to be a low-ceremony approach to building APIs in .NET. This means minimal features and minimal complexity. However, with this comes certain tradeoffs. One of these is not supporting content negotiation out of the box. It doesn’t mean we can’t support it; it just means we do some additional work.
In the next section, let’s set up our Minimal API project and attempt to consume it with different content types. Then, we can see how to add content negotiation to it.
Minimal API Project Setup
So, we have a Blog
class as our primary resource:
public class Blog { public int Id { get; init; } public DateOnly DatePublished { get; init; } public string? Title { get; init; } public string[]? Tags { get; init; } }
Then, let’s create a bare-bone .NET Minimal API, then update our Program
class:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.UseHttpsRedirection(); app.MapGet("/blogs", () => GetBlogs()); app.Run();
Finally, let’s provide a simple in-memory collection to avoid connection to a database as this is not the topic here:
private static Blog[] GetBlogs() { var blogs = new Blog[] { new() { Id = 1, DatePublished = DateOnly.FromDateTime(DateTime.Now), Title = "Building .NET Minimal API's", Tags = ["c#", ".net", "api"] }, new() { Id = 2, DatePublished = DateOnly.FromDateTime(DateTime.Now.AddDays(-1)), Title = "Content Negotiation in .NET API's", Tags = [ "c#", ".net", "api", "content negotiation"] }, ... }; return blogs; }
When we run our application and hit the /blogs
endpoint in our browser, we get a JSON result for our blog posts, as expected:
[ { "id": 1, "datePublished": "2024-09-28", "title": "Building .NET Minimal API's", "tags": ["c#", ".net", "api" ] }, { "id": 2, "datePublished": "2024-09-27", "title": "Content Negotiation in .NET API's", "tags": ["c#", ".net", "api", "content negotiation"] }, ... ]
Here, we notice the first “sensible defaults” approach to Minimal APIs. By default, we have JSON returned from our API.
If we look at the Accept
header in our browser for the request, notice the value:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
It may differ slightly depending on our browser, but notice there is no mention of application/json
. So, even though the browser didn’t ask for it, the web server returned JSON, which it didn’t ask for. That’s not very nice! However, notice the browser accepts application/xml
as a content type.
Let’s explicitly ask for application/xml
from our web server to test this out. We’ll use the built-in support for testing APIs in Visual Studio to make this a bit easier.
Testing the API
Visual Studio provides a nice feature for testing API’s by way of .http
files.
In the solution, we can see there is already a BlogsAPI.http
file.
Let’s replace the existing code:
@hostname = localhost @port = 7064 @url = https://{{hostname}}:{{port}}/blogs ### Get Blogs (JSON) GET {{url}} Accept: application/json ### Get Blogs (XML) GET {{url}} Accept: application/xml
Here, we set up two tests for the same endpoint, one for JSON and another for XML. We use some variables to avoid repeating code. Also, we can use environments and other neat tricks, but that’s outside the scope of this article.
Notice a “Send Request” and “Debug” link pair above each test. Let’s click on the first one for the JSON test and view the result on the right-hand side. Notice it’s the same response as the previous one, but we have a bit more tabs and details.
Let’s click on the second request (XML) and notice again that it is the same response as before (JSON).
Finally, let’s go to the “Request” tab. We see the Accept
header with the application/xml
value.
In other words, even though we explicitly asked for XML, our Minimal API again ignored this and returned JSON. This proves that Minimal APIs don’t support Content Negotiation by default.
But don’t worry — there is hope! Before we consider adding content negotiation to our API, it’s worth discussing how it would be possible in a regular .NET Web API.
Minimal APIs vs Web APIs
As we’ve seen, Minimal APIs don’t support content negotiation by default. But what about regular .NET Web APIs? As seen in our Content Negotiation in Web API article, again, out-of-the-box content negotiation isn’t supported by default. However, it’s possible to enable it by adding some support formatters and configuring ASP.NET Core.
This isn’t the case for Minimal APIs. We can explicitly support a particular content type but not both. If we want to support both, we need to do a fair bit of work (or use Carter, as we’ll learn in a moment).
But back to our scenario: We love Minimal API, but we still want content negotiation. Instead of writing a bunch of code to solve the task, let’s use our great community and, in the next section, leverage the awesome Carter library to do it for us.
What Is Carter?
Carter is a library that provides a thin layer of extension methods and functionality over ASP.NET Core, making the code more explicit and, most importantly, more enjoyable.
Carter was designed to make our lives easier by doing the common things we do. Things like validation, authorization, and file uploading can all be done with Web APIs or Minimal APIs, but Carter lets us do it with fewer lines of code.
Exploring all of Carter’s features would be an article in itself, but since we are focused on content negotiation, let’s use Carter for that.
Using Carter for Content Negotiation
As we’ve gathered, content negotiation in Minimal APIs is no easy feat. Carter has written most of that code for us, allowing us to focus on our application logic. While we still need to write code to return the various content, the negotiation aspect (interrogating the Accept
header, picking the right negotiator, etc.) is the complex bit that Carter has taken care of for us.
Let’s see some code in action!
Adding Carter for Content Negotiation
First, we need to add the Carter package from NuGet:
dotnet add package carter
Then, we can make a couple of simple changes to the Program
class:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddCarter(); var app = builder.Build(); app.MapCarter(); app.UseHttpsRedirection(); app.Run();
The main change here is we use the AddCarter()
and MapCarter()
extension methods to wire up Carter to our application.
Let’s now add another class called BlogModule
:
public class BlogModule : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { app.MapGet("/blogs", () => GetBlogs()); } public Blog[] GetBlogs() { // same as previous } }
Here, we extract our route into a class that implements the ICarterModule
interface. This is how Carter registers endpoints.
If we run our HTTP
requests again, everything still works as expected. However, that includes the XML request, which is still returning JSON. Not for long!
In the next section, let’s set up the XML content negotiation.
Setting up a Custom Content Negotiator
There are a few steps to add support for XML. First, we need to set up a custom negotiator. This is what will serialize the response to the particular content type. We need to create a custom negotiator for each content type we want to support. Since Minimal APIs already support JSON out of the box, let’s look at adding an XML negotiator.
Let’s create a XmlNegotiator
class:
public class XmlNegotiator : IResponseNegotiator { private const string _xmlMediaType = "application/xml"; public bool CanHandle(MediaTypeHeaderValue accept) => accept.MatchesMediaType(_xmlMediaType); public async Task Handle<T>(HttpRequest req, HttpResponse res, T model, CancellationToken cancellationToken) { res.ContentType = _xmlMediaType; var serializer = new DataContractSerializer(model.GetType()); using var ms = StreamManager.Instance.GetStream(); serializer.WriteObject(ms, model); ms.Position = 0; await ms.CopyToAsync(res.Body, cancellationToken); } } public static class StreamManager { public static readonly RecyclableMemoryStreamManager Instance = new(); }
Here, we implement two methods from the IResponseNegotiator
interface. First, the CanHandle()
method is used to inform Carter of the content type handled by this class, which is "application/xml"
. Second, the Handle()
method is employed to serialize the incoming model into the desired content.
We are also making use of a StreamManager
, to improve the code’s performance. Without this, we would introduce unnecessary garbage collection from all the various streams.
Next, we need to wire this up.
Wiring up Our Custom Content Negotiator
To wire up our negotiator, we need a small change in our Program
class:
builder.Services.AddCarter(configurator: c => { c.WithResponseNegotiator<XmlNegotiator>(); });
This tells Carter that we have a custom negotiator that can handle XML.
Finally, we need to update our module.
To get our module to be able to return XML, again, it’s a small change to our BlogModule
class:
public class BlogModule : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { app.MapGet("/blogs", (HttpResponse response) => response.Negotiate(GetBlogs())); } }
Here, instead of just returning the Blog
data, we are calling the Negotiate()
method on the incoming HttpResponse
object. This tells Carter to look at the Accept
header and attempt to negotiate the response based on what’s configured.
We’re done! Now, let’s test out our changes.
Running the .http Tests
If we run the XML request in our .http file, notice we now have XML returned. If we run our JSON request, we still get JSON back.
How does that work, even if we haven’t added a custom negotiator for JSON?
When calling the Negotiate()
method, Carter will look for the most suitable negotiator for the Accept
header supplied. If it doesn’t find a match, it falls back to the default, which is JSON. So, there is no need to create a custom formatter for JSON.
That’s all there is to it! We now have a .NET Minimal API capable of returning both JSON and XML. If we want to support other content types, it would just be a matter of adding a new implementation of IResponseNegotiator
and registering it with Carter.
Let’s now zoom back and discuss some of the tradeoffs and considerations when using Carter for content negotiation.
Pros and Cons of Using Carter for Content Negotiation
We saw that using Carter for content negotiation in .NET is relatively straightforward. There are several advantages and disadvantages to consider.
One advantage is that built-in support for negotiation is provided through the IResponseNegotiator
interface, which allows for a reduction in boilerplate code. Additionally, new content type support can be added easily, and the code is kept clean, minimal, and lightweight.
However, there are also disadvantages.
ASP.NET Core MVC supports XML content negotiation out of the box, and Carter does not.
Carter has a smaller community than ASP.NET Core MVC, resulting in a smaller set of extensions.
Finally, Carter’s conventions, such as the requirement to use modules, must be adhered to.
So, how do we decide?
When to Use and Avoid Carter for Content Negotiation
Choosing Carter for Content Negotiation over regular ASP.NET Web API requires a few considerations.
If minimal code, flexibility, and a modular approach to our application are important, Carter is likely a good choice. While these considerations aren’t specific to content negotiation but more to Carter, they still apply here.
If we want more support for different formats (e.g., XML, JSON, CSV) out of the box and a richer ecosystem and set of extensions, then ASP.NET Core MVC is probably a better choice.
Conclusion
In this article, we briefly discussed content negotiation and how easy it is to implement using Carter. Like any technology choice, it comes with a set of tradeoffs and considerations, so it’s ultimately up to us to choose the best fit for the use case. Hopefully, this article will help you make that decision more quickly.