I think it's safe to say that most ASP.NET Core applications that use a Web API return data as JSON. What with JavaScript in the browser, and JSON parsers everywhere you look, this makes perfect sense. Consequently, ASP.NET Core is very much geared towards JSON, but it is perfectly possible to return data in other formats (for example Damien Bowden recently added a Protobuf formatter to the WebApiContrib.Core project).
In this post, I'm going to focus on a very specific scenario. You want to be able to return data from a Web API action method in one of two different formats - JSON or XML, and you want to control which format is used by the extension of the URL. For example /api/Values.xml
should format the result as XML, while /api/Values.json
should format the result as JSON.
Using the FormatFilterAttribute
to read the format from the URL
Out of the box, if you use the standard MVC service configuration by calling services.AddMvc()
, the JSON formatters are configured for your application by default. All that you need to do is tell your action method to read the format from the URL using the FormatFilterAttribute
.
You can add the [FormatFilter]
attribute to your action methods, to indicate that the output format will be defined by the URL. The FormatFilter
looks for a route parameter called format
in the RouteData
for the request, or in the querystring. If you want to use the .json
approach I described earlier, you should make sure the route template for your actions includes a .{format}
parameter:
public class ValuesController : Controller
{
// GET api/values
[HttpGet("api/values.{format}"), FormatFilter]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
Note: You can make the
.format
suffix optional using the syntax.{format?}
, but you need to make sure the . follows a route parameter, e.g.api/values/{id}.{format?}
. If you try to make the format optional in the example above (api/values.{format?}
) you'll get a server error. A bit odd, but there you go…
With the route template updated, and the [FormatFilter]
applied to the method, we can now test our JSON formatters:
Success - we have returned JSON when requested! Let's give it a try with the xml
suffix:
Doh, no such luck. As I mentioned earlier, the JSON formatters are registered by default; if we want to return XML then we'll need to configure the XML formatters too.
Adding the XML formatters
In ASP.NET Core, everything is highly modular, so you only add the functionality you need to your application. Consequently, there's a separate NuGet package for the XML formatters that you need to add to your .csproj file - Microsoft.AspNetCore.Mvc.Formatters.Xml
<PackageReference Include="Microsoft.AspNetCore.Mvc.Formatters.Xml" Version="1.1.3" />
Note: If you're using ASP.NET Core 2.0, this package is included by default as part of the
Microsoft.AspNetCore.All
metapackage.
Adding the package to your project lights up an extension method on the IMvcBuilder
instance returned by the call to services.AddMvc()
. The AddXmlSerializerFormatters()
method adds both input and output formatters, so you can serialise objects to and from XML.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddXmlSerializerFormatters();
}
Alternatively, if you only want to be able to format results as XML, but don't need to be able to read XML from a request body, you can just add the output formatter instead:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
});
}
By adding this output formatter we can now format objects as XML. However, if you test the XML URL again at this point, you'll still get the same 404 response as we did before. What gives?
Registering a type mapping for the format suffix
By registering the XML formatters, we now have the ability to format XML. However, the FormatFilter
doesn't know how to handle the .xml
suffix we're using in the request URL. To make this work, we need to tell the filter that the xml
suffix maps to the application/xml
MIME type.
You can register new type mappings by configuring the FormatterMappings
options, when you call AddMvc()
. These define the mappings between the {format}
parmeter and the MIME type that the FormatFilter
will use. For example:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.FormatterMappings.SetMediaTypeMappingForFormat
("xml", MediaTypeHeaderValue.Parse("application/xml"));
options.FormatterMappings.SetMediaTypeMappingForFormat
("config", MediaTypeHeaderValue.Parse("application/xml"));
options.FormatterMappings.SetMediaTypeMappingForFormat
("js", MediaTypeHeaderValue.Parse("application/json"));
})
.AddXmlSerializerFormatters();
The FormatterMappings
property contains a dictionary of all the suffix to MIME type mappings. You can add new ones using the SetMediaTypeMappingForFormat
, passing the suffix as the key and the MIME type as the value.
In the example above I've actually registered three new mappings. I've added the xml
and config
files as XML, and added a new js
suffix that maps to JSON, just to demonstrate that JSON isn't actually anything special here!
With this last piece of configuration in place, we can now finally request XML by using the .xml
or .config
suffix in our URLs:
Summary
In this post you saw how to use the FormatFilter
to specify the desired output format by using a file-type suffix on your URLs. To do so, there were four steps:
- Add the
[FormatFilter]
attribute to your action method - Ensure the route to the action contains a
{format}
route parameter (or pass it in the querystirng e.g.?format=xml
) - Register the output formatters you wish to support with MVC. To add both input and output XML formatters, use the
AddXmlSerializerFormatters()
extensions method - Register a new type mapping between a format suffix and a MIME type on the MvcOptions object. For example, you could add XML using:
options.FormatterMappings.SetMediaTypeMappingForFormat(
"xml", MediaTypeHeaderValue.Parse("application/xml"));
If a type mapping is not configured for a suffix, then you'll get a 404 Not Found
response when calling the action.