How to Implement Content Negotiation in ASP.NET Core 2.0
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. Learn how to use it!
Join the DZone community and get the full member experience.
Join For FreeFor the complete and up-to-date version of the article visit: https://code-maze.com/content-negotiation-dotnet-core/
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 which 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.
Today, we are going through the content negotiation implementation in ASP.NET Core.
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 ValuesController. 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
andBlogPosts
to create an object to return as a response object - 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 you can override it by changing configuration options through the AddMvc method options. By default, it looks like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
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.AddMvc(config =>
{
// Add XML Content Negotiation
config.RespectBrowserAcceptHeader = true;
config.InputFormatters.Add(new XmlSerializerInputFormatter());
config.OutputFormatters.Add(new XmlSerializerOutputFormatter());
});
}
First things first, we must tell a server to respect the Accept header. After that, we can add XML formatters to enable the XML formatting.MvcXmlSerializerOutputFormatter
and are both part of the Microsoft.AspNetCore.Mvc.Formatters, so we need to add a reference to that library.
Now that we have our server configured let's test the content negotiation once more.
Testing the 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
Currently, it will default to a JSON type.
But you can restrict this behavior by adding one line to the configuration.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(config =>
{
config.RespectBrowserAcceptHeader = true;
config.ReturnHttpNotAcceptable = true;
config.InputFormatters.Add(new XmlSerializerInputFormatter());
config.OutputFormatters.Add(new XmlSerializerOutputFormatter());
});
}
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 and OutputFormatters 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
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 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);
}
using (var writer = context.WriterFactory(response.Body, selectedEncoding))
{
return writer.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 the Blog 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.AddMvc(config =>
{
config.RespectBrowserAcceptHeader = true;
config.ReturnHttpNotAcceptable = true;
config.InputFormatters.Add(new XmlSerializerInputFormatter());
config.OutputFormatters.Add(new XmlSerializerOutputFormatter());
config.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 APIs 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.
Published at DZone with permission of Vladimir Pecanac. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments