Content negotiation is one of those quality-of-life improvements you can add to your REST API to make it more user-friendly and flexible. And when we design an API, isn’t that what we want to achieve in the first place?
There are many things to keep in mind when designing aĀ REST API and we’ve written recently about it in our Top REST APIĀ best practices article. Content negotiation is an HTTP feature that has been around for a while, but for one reason or another, it is, maybe, a bit underused.
In short, content negotiation lets you choose or rather “negotiate” the content you want in to get in response to the REST API request. If you want to learn how content negotiation works behind the scenes, you can download our CompleteĀ Guide to HTTP Book for free and look it up in the advanced features section.
You can download the source code from our Content Negotiation repository.
Today, we are going through the content negotiation implementation in ASP.NET Core.
We are going to talk about:
- What You Get out of the Box With ASP.NET Core 2.0
- How to Use Postman to Test Your API
- Changing the Default Configuration of Our Project
- Testing Content Negotiation
- Restricting Media Types
- More About Formatters
- Implementing a Custom Formatter
- Consuming API Programmatically
So let’s get down to it.
What Do You Get out of the Box?
By default, ASP.NET Core Web APIĀ returns a JSON formatted result.
Let’s make a default Web API project and remove the default WeatherForecastController
. Instead, we are going to make our own controller (with blackjack and hookers), BlogController with only one method:
[Route("api/[controller]")] public class BlogController : Controller { public IActionResult Get() { var blogs = new List<Blog>(); var blogPosts = new List<BlogPost>(); blogPosts.Add(new BlogPost { Title = "Content negotiation in .NET Core", MetaDescription = "Content negotiation is one of those quality-of-life improvements you can add to your REST API to make it more user-friendly and flexible. And when we design the API, isn't that what we want to achieve in the first place?", Published = true }); blogs.Add(new Blog() { Name = "Code Maze", Description = "A practical programmers resource", BlogPosts = blogPosts }); return Ok(blogs); } }
Things to note about this simple example:
- We are using two classes: Blog and BlogPosts to create an object to return as a response object (you can find these classes in the Models folder in our source code)
- We are utilizing the IActionResult interface provided by ASP.NET Core as a generic return type for different types of responses our methods might have
- The object creation logic is in the controller. You should not implement your controllers like this; this is just for the sake of simplicity
- We are returning the result with the Ok helper method which returns the object and the status code
200 OK
How to Use Postman to Test Your API
Postman is a nice little tool you can use to test your APIs easily. Now, let’s try calling the method using Postman and see whatĀ we get as a response.
You can clearlyĀ see that the default result when calling GET on /api/blog
returns our JSON result. Those of you with sharp eyes might have even noticed that we used theĀ Accept
header to try forcing the server to return other media types like plain text and XML.
But that doesn’t work. Why?
Because we need to configure server formatters to format a response the way we want it.
Let’s see how to do that.
Changing the Default Configuration of Our Project
A server does not explicitly specify where it formats a response to JSON. But we can override it by changing configuration options through the AddControllers
Ā method options. By default, it looks like this:
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); }
We can add the following options to enable the server to format the XML response when the client tries negotiating for it.
public void ConfigureServices(IServiceCollection services) { services.AddControllers(config => { config.RespectBrowserAcceptHeader = true; }).AddXmlDataContractSerializerFormatters(); }
First things first, we must tell a server to respect the Accept header. After that, we just add the
AddXmlDataContractSerializerFormatters
method to support XML formatters.
Now that we have our server configured let’s test the content negotiation once more.
Testing Content Negotiation
Let’s see what happens now if we fire the same request through Postman.
There is our XML response.
That was easy, wasn’t it?
Now by changing the Accept header from text/xml
to text/json
, we can get differently formatted responses which is awesome, wouldn’t you agree?
Ok, that was nice and easy.
But what if despite all this flexibility a client requests a media type that a server doesn’t know how to format?
Restricting Media Types in Content Negotiation
Currently, it will default to a JSON type.
But we can restrict this behavior by adding one line to the configuration.
public void ConfigureServices(IServiceCollection services) { services.AddControllers(config => { config.RespectBrowserAcceptHeader = true; config.ReturnHttpNotAcceptable = true; }).AddXmlDataContractSerializerFormatters(); }
We added the
ReturnHttpNotAcceptable = true
option, which tells the server that if the client tries to negotiate for the media type the server doesn’t support, it should return the 406 Not Acceptable status code.
This will make your application more restrictive and force the API consumer to request only the types the server supports. The 406 status code is created for this purpose. You can find more details about that in ourĀ CompleteĀ Guide to HTTP book, or if you want to go even deeper you can check out the RFC2616.
Now, let’s try fetching the text/css
media type using Postman to see what happens.
And as expected, there is no response body, and all we get is a nice 406 Not Acceptable
status code.
So far so good.
More About Formatters
Let’s imagine you are making a public REST API and it needs to support content negotiation for a type that is not “in the box”. Rare as it might occur, you need to have a mechanism to do this.
So, how can you do that?
ASP.NET Core supports the creation of custom formatters. Their purpose is to give you the flexibility to create your own formatter for any media types you need to support.
We can make the custom formatter using the following method:
- Create an output formatter class that inherits the
TextOutputFormatter class - Create an input formatter class that inherits the
TextInputformatter class - Add input and output classes to
InputFormatters
andOutputFormatters
collections the same way as we did for the XML formatter
Now let’s have some fun and implement a custom CSV formatter for our example.
Implementing a Custom Formatter for Content Negotiation
Since we are only interested in formatting responses in this article, we need to implement only an output formatter. We would need an input formatter only if a request body contained a corresponding type.
The idea is to format a response to return the list of blogs and their corresponding list of blog posts in a CSV format.
Let’s add a CsvOutputFormatter
class to our project.
public class CsvOutputFormatter : TextOutputFormatter { public CsvOutputFormatter() { SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/csv")); SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.Unicode); } protected override bool CanWriteType(Type type) { if (typeof(Blog).IsAssignableFrom(type) || typeof(IEnumerable<Blog>).IsAssignableFrom(type)) { return base.CanWriteType(type); } return false; } public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) { var response = context.HttpContext.Response; var buffer = new StringBuilder(); if (context.Object is IEnumerable<Blog>) { foreach (var Blog in (IEnumerable<Blog>)context.Object) { FormatCsv(buffer, Blog); } } else { FormatCsv(buffer, (Blog)context.Object); } await response.WriteAsync(buffer.ToString()); } private static void FormatCsv(StringBuilder buffer, Blog blog) { foreach (var blogPost in blog.BlogPosts) { buffer.AppendLine($"{blog.Name},\"{blog.Description},\"{blogPost.Title},\"{blogPost.Published}\""); } } }
There are a few things to note here:
- In the constructor, we define which media type this formatter should parse as well as encodings
- TheĀ
CanWriteType
method is overridden, and it indicates whether or not theBlog
Ā type can be written by this serializer. - TheĀ
WriteResponseBodyAsync
Ā method that constructs the response - And finally, we have the
FormatCsv
method that formats a response the way we want it.
The class is pretty straightforward to implement, and the main thing that you should focus on is the FormatCsv
method logic.
Now, we just need to add the newly made formatter to the list of OutputFormatters
in the AddMvcOptions
.
public void ConfigureServices(IServiceCollection services) { services.AddControllers(config => { config.RespectBrowserAcceptHeader = true; config.ReturnHttpNotAcceptable = true; }).AddXmlDataContractSerializerFormatters() .AddMvcOptions(c => c.OutputFormatters.Add(new CsvOutputFormatter())); }
Now let’s run this and see if it actually works. This time we will put the
text/csv
as the value for the Accept
header.
Well, what do you know, it works!
Since we only have one blog and one blog post in our example, there is only one line in the response.
You can play around with source code to see what happens when you add more blogs and blog posts.
There is a greatĀ page about custom formatters in ASP.NET Core if you want to learn more about them. You can also check out the implementation of the input and output formatters for the vcardĀ content type if you need more examples.
Consuming API Programmatically
Up until now, we have used Postman to play around with the example. But, I feel you need to try out to consume some REST APIs using content negotiation we described here by making some requests programmatically instead of using the third party tool.
For that purpose, we have laid out a few great ways to consume RESTful API. You can find some of the best tools that .NET provides to consume any REST API. Be sure to check it out and try consuming some APIs.
Conclusion
In this blog post, we went through a concrete implementation of the content negotiation mechanism in an ASP.NET Core project. We have learned about formatters and how to make a custom one, and how to set them up in your project configuration as well.
We have also learned how to restrict an application only to certain content types, and not accept any others.
You should be able both to design and consume REST APIs using content negotiation now. It really is a great mechanism, and we have great tools to implement it in our projects, easily. So, there are no excuses!
If you want to play around with the source code, you can find it here: Download source code from GitHub.
Thanks for reading and please leave a comment in the comment section.
Thank you
Hey Richard, glad it helped!
In AspNetCore 2.2, if you manually add the XmlFormatters to the config like you have, then it fails to correctly serialize errors, but if you add them by calling AddXmlSerializerFormatters on the IMvcBuilder, then it all works. Like:
services.AddMvc(config=>{ ... stuff here ...}).AddXmlSerializerFormatters().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
Hi Robert, great information, thanks!
Do you have an example or a blog post that shows off this behavior?