Media-Type Formatting CLR Objects
From the ASP.NET Web API perspective, serialization is the process of translating a .NET Common Language Runtime (CLR) type into a format that can be transmitted over HTTP. The format is either JSON or XML, out of the box. A media type formatter, which is an object of type MediaTypeFormatter, performs the serialization in the ASP.NET Web API pipeline. Consider a simple action method handling GET in an ApiController:
public Employee Get(int id)
{
return list.First(e => e.Id == id);
}
This method returns a CLR object of type Employee. In order for the data contained in this object to be returned to the client in the HTTP response message, the object must be serialized. The MediaTypeFormatter object in the ASP.NET Web API pipeline performs this serialization. It serializes the object returned by the action method into JSON or XML, which is then written into the response message body. The out-of-box media formatters that produce JSON and XML are respectively JsonMediaTypeFormatter and XmlMediaTypeFormatter, both deriving from MediaTypeFormatter. The process through which the MediaTypeFormatter is chosen is called content negotiation , commonly shortened to conneg .
A resource can have one or more representations. When you issue a GET to retrieve a resource, such as the employee with ID 12345, the response message contains the representation of the resource, which is a specific employee in this case. The Web API indicates how the resource is represented in the response through the Content-Type response header. The Accept request header can be used by a client to indicate the set of preferred representations for the resource in the response.
Out of the box, the ASP.NET Web API framework supports two media or content types: JSON and XML. If you send a request with Accept: application/json, the response message will be JSON and Content-Type will be application/json. Similarly, if you send a request with Accept: application/xml, the response message will be XML. You can also specify a quality value indicating the relative preference. The range is 0–1, with 0 being unacceptable and 1 being the most preferred. The default value is 1. For example, if you send the request header Accept: application/json; q=0.8, application/xml;q=0.9, the response message will be XML, because application/xml has a quality value of 0.9, which is higher than the quality value of 0.8 specified for application/json.
3.1 Listing the Out-of-Box Media Formatters
In this exercise, you will list the media type formatters that come out of the box with ASP.NET Web API.
public class Employee
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Department { get; set; }
}
Listing 3-1. Listing Media Formatters
using System;
using System.Diagnostics;
using System.Web.Http;
using HelloWebApi.Models;
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
foreach (var formatter in config.Formatters)
{
Trace.WriteLine(formatter.GetType().Name);
Trace.WriteLine(" CanReadType: " + formatter.CanReadType(typeof(Employee)));
Trace.WriteLine(" CanWriteType: " + formatter.CanWriteType(typeof(Employee)));
Trace.WriteLine(" Base: " + formatter.GetType().BaseType.Name);
Trace.WriteLine(" Media Types: " + String.Join(", ", formatter. SupportedMediaTypes));
}
}
} // Output
JsonMediaTypeFormatter
CanReadType: True
CanWriteType: True
Base: MediaTypeFormatter
Media Types: application/json, text/json
XmlMediaTypeFormatter
CanReadType: True
CanWriteType: True
Base: MediaTypeFormatter
Media Types: application/xml, text/xml
FormUrlEncodedMediaTypeFormatter
CanReadType: False
CanWriteType: False
Base: MediaTypeFormatter
Media Types: application/x-www-form-urlencoded
JQueryMvcFormUrlEncodedFormatter
CanReadType: True
CanWriteType: False
Base: FormUrlEncodedMediaTypeFormatter
Media Types: application/x-www-form-urlencoded
From the serialization point of view, the last two media type formatters can be ignored, since they cannot write any type. The first two, JsonMediaTypeFormatter and XmlMediaTypeFormatter, are the important ones. They are the media formatters that produce JSON and XML resource representations in the response.
3.2 Understanding Conneg
This exercise demonstrates how the process of content negotiation works. Content negotiation is the process by which ASP.NET Web API chooses the formatter to use and the media type for the response message.
The System.Net.Http.Formatting.DefaultContentNegotiator class implements the default conneg algorithm in the Negotiate method that it implements, as part of implementing the IContentNegotiatior interface. This method accepts three inputs:
The Negotiate method checks the following four items before deciding on the media formatter to use, in descending order of precedence:
Try the following steps to see for yourself how ASP.NET Web API conneg works.
Listing 3-2. An ApiController With an Action-Method–Handling GET
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using HelloWebApi.Models;
public class EmployeesController : ApiController
{
private static IList<Employee> list = new List<Employee>()
{
new Employee()
{
Id = 12345, FirstName = "John", LastName = "Human"
},
new Employee()
{
Id = 12346, FirstName = "Jane", LastName = "Public"
},
new Employee()
{
Id = 12347, FirstName = "Joseph", LastName = "Law"
}
};
// GET api/employees/12345
public Employee Get(int id)
{
return list.First(e => e.Id == id);
}
}
Request |
GET http://localhost:55778/api/employees/12345 HTTP/1.1 Accept: application/json Host: localhost:55778 |
Response |
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 {"Id":12345,"FirstName":"John","LastName":"Human"} |
Request |
GET http://localhost:55778/api/employees/12345 HTTP/1.1 Content-Type: application/xml Host: localhost:55778 |
Response |
HTTP/1.1 200 OK Content-Type: application/xml; charset=utf-8 <Employee xmlns:i=" http://www.w3.org/2001/XMLSchema-instance" xmlns=" http://schemas.datacontract.org/2004/07/HelloWebApi.Models "> <FirstName>John</FirstName> <Id>12345</Id> <LastName>Human</LastName> </Employee> |
Request |
GET http://localhost:55778/api/employees/12345 HTTP/1.1 Accept: application/xml;q=0.2, application/json;q=0.8 Host: localhost:55778 |
Response |
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 ... |
Request |
GET http://localhost:55778/api/employees/12345 HTTP/1.1 Content-Type: application/xml Host: localhost:55778 |
Response |
HTTP/1.1 200 OK Content-Type: application/xml; charset=utf-8 ... |
Request | GET http://localhost:55778/api/employees/12345 HTTP/1.1 Host: localhost:55778 |
Response |
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 ... |
config.Formatters.RemoveAt(0);
as shown in Listing 3-3. This removes JsonMediaTypeFormatter, which is the first formatter in the Formatters collection.
Listing 3-3. Removing a Media Formatter
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Formatters.RemoveAt(0);
foreach (var formatter in config.Formatters)
{
Trace.WriteLine(formatter.GetType().Name);
Trace.WriteLine(" CanReadType: " + formatter.CanReadType(typeof(Employee)));
Trace.WriteLine(" CanWriteType: " + formatter.CanWriteType(typeof(Employee)));
Trace.WriteLine(" Base: " + formatter.GetType().BaseType.Name);
Trace.WriteLine(" Media Types: " + String.Join(", ", formatter. SupportedMediaTypes));
}
}
}
Request |
GET http://localhost:55778/api/employees/12345 HTTP/1.1 Accept: application/json Host: localhost:55778 |
Response |
HTTP/1.1 200 OK Content-Type: application/xml; charset=utf-8 ... |
Request |
GET http://localhost:55778/api/employees/12345 HTTP/1.1 Host: localhost:55778 |
Response |
HTTP/1.1 406 Not Acceptable Content-Length: 0 |
config.Formatters.RemoveAt(0);
3.3 Requesting a Content Type through the Query String
In the previous exercise, you saw conneg in action. One piece that was missing, however, was the media type mapping, which occupies the top slot in the order of precedence. If the conneg algorithm finds a matching media type based on this mapping, and if the corresponding media type formatter is capable of serializing the type, no more matching is done. The matching media type based on the media type mapping will be used as the media type for the response message. In this exercise, you will see how to request a content type through media type mapping based on a query string.
Listing 3-4. Media Type Mapping Based on Query String
using System;
using System.Diagnostics;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Web.Http;
using HelloWebApi.Models;
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Formatters.JsonFormatter .MediaTypeMappings.Add(
new QueryStringMapping("frmt", "json",
new MediaTypeHeaderValue("application/json")));
config.Formatters.XmlFormatter .MediaTypeMappings.Add(
new QueryStringMapping("frmt", "xml",
new MediaTypeHeaderValue("application/xml")));
foreach (var formatter in config.Formatters)
{
Trace.WriteLine(formatter.GetType().Name);
Trace.WriteLine(" CanReadType: " + formatter.CanReadType(typeof(Employee)));
Trace.WriteLine(" CanWriteType: " + formatter.CanWriteType(typeof(Employee)));
Trace.WriteLine(" Base: " + formatter.GetType().BaseType.Name);
Trace.WriteLine(" Media Types: " + String.Join(", ", formatter.
SupportedMediaTypes));
}
}
}
Request |
GET http://localhost:55778/api/employees/12345?frmt=json HTTP/1.1 Accept: application/xml Host: localhost:55778 |
Response |
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 {"Id":12345,"FirstName":"John","LastName":"Human"} |
Request |
GET http://localhost:55778/api/employees/12345?frmt=xml HTTP/1.1 Accept: application/json Host: localhost:55778 |
Response |
HTTP/1.1 200 OK Content-Type: application/xml; charset=utf-8 ... |
3.4 Requesting a Content Type through the Header
In this exercise, you will see how a content type can be requested through the media type mapping based on the request header.
Listing 3-5. Media Type Mapping Based on Request Header
config.Formatters.JsonFormatter
.MediaTypeMappings.Add(
new RequestHeaderMapping(
"X-Media", "json",
StringComparison.OrdinalIgnoreCase, false,
new MediaTypeHeaderValue("application/json")));
Request |
GET http://localhost:55778/api/employees/12345 HTTP/1.1 Accept: application/xml X-Media: json Host: localhost:55778 |
Response |
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 {"Id":12345,"FirstName":"John","LastName":"Human"} |
Request |
GET http://localhost:55778/api/employees/12345?frmt=xml HTTP/1.1 Accept: application/xml X-Media: json Host: localhost:55778 |
Response |
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 {"Id":12345,"FirstName":"John","LastName":"Human"} |
3.5 Implementing a Custom Media Type Mapping
In this exercise, you will create a custom media type mapping class that derives from MediaTypeMapping to map the IP address of the client to a media type. For all the requests coming from the local machine with loopback address of ::1 (IPv6), JSON will be the media type, regardless of the values in the Accept and Content-Type headers.
Listing 3-6. Custom Media Type Mapping
using System;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.ServiceModel.Channels;
using System.Web;
public class IPBasedMediaTypeMapping : MediaTypeMapping
{
public IPBasedMediaTypeMapping() :
base(new MediaTypeHeaderValue("application/json")) { }
public override double TryMatchMediaType(HttpRequestMessage request)
{
string ipAddress = String.Empty;
if (request.Properties.ContainsKey("MS_HttpContext"))
{
var httpContext = (HttpContextBase)request.Properties["MS_HttpContext"];
ipAddress = httpContext.Request.UserHostAddress;
}
else if (request.Properties.ContainsKey(RemoteEndpointMessageProperty.Name))
{
RemoteEndpointMessageProperty prop;
prop = (RemoteEndpointMessageProperty)
request.Properties[RemoteEndpointMessageProperty.Name];
ipAddress = prop.Address;
}
//::1 is the loopback address in IPv6, same as 127.0.0.1 in IPv4
// Using the loopback address only for illustration
return "::1".Equals(ipAddress) ? 1.0 : 0.0;
}
}
Listing 3-7. Registering the Custom Media Type Mapping
config.Formatters.JsonFormatter
.MediaTypeMappings.Add(new IPBasedMediaTypeMapping());
Request |
GET http://localhost:55778/api/employees/12345 HTTP/1.1 Accept: application/xml Host: localhost:55778 |
Response |
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 {"Id":12345,"FirstName":"John","LastName":"Human"} |
Listing 3-8. The Default WebApiConfig Class
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
3.6 Overriding Conneg and Returning JSON
In this exercise, you will override conneg and let ASP.NET Web API use the media formatter that you specify to serialize the resource. The key is to manually return HttpResponseMessage after setting the Content to ObjectContent<T>, specifying the media formatter. In the following example, you will specify that the Employee object must always be serialized as JSON using JsonMediaTypeFormatter, regardless of what conneg comes up with. The formatter you specify here takes precedence over the formatter determined by conneg.
Listing 3-9. Overriding Conneg
public HttpResponseMessage Get(int id)
{
var employee = list.FirstOrDefault(e => e.Id == id);
return new HttpResponseMessage()
{
Content = new ObjectContent<Employee>(employee,
Configuration.Formatters.JsonFormatter)
};
}
3.7 Piggybacking on Conneg
In this exercise, you will manually run conneg , similar to the way the ASP.NET Web API framework runs, and take action based on what conneg comes up with. Here is a scenario where manual conneg will be handy. Suppose your web API is consumed by multiple external client applications, over which you have no control. You support multiple media types, and you charge the web API consumers based on the egress traffic (response message size). One consumer has asked you to blacklist a specific media type, say XML. One way to meet this requirement is by removing the XmlMediaTypeFormatter altogether, as we did in Exercise 3.2. But this will not be desirable when other consumers do need XML. Another option is to hard-code a specific formatter other than that of XML, as we did in Exercise 3.6. But the drawback in that case is that the customer would still want the ability to conneg between the available options other than XmlMediaTypeFormatter. A simple solution to meet this need will be to manually run conneg after removing the media type formatters that support the blacklisted media type. Modify the Get action method, as shown in Listing 3-10.
Listing 3-10. Piggybacking on Conneg
public HttpResponseMessage Get(int id)
{
// hard-coded for illustration but for the use case described,
// the blacklisted formatter might need to be retrieved from
// a persistence store for the client application based on some identifier
var blackListed = "application/xml";
var allowedFormatters = Configuration.Formatters
.Where(f => !f.SupportedMediaTypes
.Any(m => m.MediaType
.Equals(blackListed,
StringComparison.OrdinalIgnoreCase)));
var result = Configuration.Services
.GetContentNegotiator()
.Negotiate(
typeof(Employee), Request, allowedFormatters);
if (result == null)
throw new HttpResponseException(System.Net.HttpStatusCode.NotAcceptable);
var employee = list.First(e => e.Id == id); // Assuming employee exists
return new HttpResponseMessage()
{
Content = new ObjectContent<Employee>(
employee,
result.Formatter,
result.MediaType)
};
}
3.8 Creating a Custom Media Formatter
ASP.NET Web API comes with two out-of-the-box media formatters: JsonMediaTypeFormatter and XmlMediaTypeFormatter, for JSON and XML media types, respectively. They both derive from MediaTypeFormatter. It is possible to create your own media formatter to handle other media types. To create a media formatter, you must derive from the MediaTypeFormatter class or the BufferedMediaTypeFormatter class. The BufferedMediaTypeFormatter class also derives from the MediaTypeFormatter class, but it wraps the asynchronous read and write methods inside synchronous blocking methods. Deriving from the BufferedMediaTypeFormatter class and implementing your custom media formatter is easier, because you do not have to deal with asynchrony, but the downside is that the methods are blocking and can create performance bottlenecks in performance-demanding applications that lend themselves well for asynchrony.
One of the benefits of using HTTP service is reachability. The consumer of your service can be from any platform. In a typical enterprise, a variety of technologies both new and legacy co-exist and work together to meet the demands of the business. Though XML or JSON parsing is available in most platforms, there are times when you will want to go back to the last century and create a fixed-width text response for specific client applications such as one running in mainframe. A fixed-width text file contains fields in specific positions within each line. These files are the most common in mainframe data feeds going both directions, because it is easier to load them into a mainframe dataset for further processing. In this exercise, you will create a fixed-width text response by creating a custom media formatter, for the client application running in a mainframe to perform a GET and load the response into a dataset.
The fixed-width text response we create will take this format: Employee ID will be 6 digits and zero-prefixed, followed by the first name and the last name. Both the names will have a length of 20 characters padded with trailing spaces to ensure the length. Thus, a record for an employee John Human with ID of 12345 will be 012345John<followed by 16 spaces>Human<followed by 15 spaces>.
Listing 3-11. The Employee Class – The Basic Version with Three Properties
public class Employee
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Listing 3-12. The Action Method to Get Employee Data
public class EmployeesController : ApiController
{
private static IList<Employee> list = new List<Employee>()
{
new Employee()
{
Id = 12345, FirstName = "John", LastName = "Human"
},
new Employee()
{
Id = 12346, FirstName = "Jane", LastName = "Public"
},
new Employee()
{
Id = 12347, FirstName = "Joseph", LastName = "Law"
}
};
// GET api/employees
public IEnumerable<Employee> Get()
{
return list;
}
}
Listing 3-13. A Custom Media Type Formatter Class
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using HelloWebApi.Models;
public class FixedWidthTextMediaFormatter : MediaTypeFormatter
{
public FixedWidthTextMediaFormatter()
{
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
}
public override bool CanReadType(Type type)
{
return false;
}
public override bool CanWriteType(Type type)
{
return typeof(IEnumerable<Employee>)
.IsAssignableFrom(type);
}
public override async Task WriteToStreamAsync(
Type type,
object value,
Stream stream,
HttpContent content,
TransportContext transportContext)
{
using (stream)
{
Encoding encoding = SelectCharacterEncoding(content.Headers);
using (var writer = new StreamWriter(stream, encoding))
{
var employees = value as IEnumerable<Employee>;
if (employees != null)
{
foreach (var employee in employees)
{
await writer.WriteLineAsync(
String.Format("{0:000000}{1,-20}{2,-20}",
employee.Id,
employee.FirstName,
employee.LastName));
}
await writer.FlushAsync();
}
}
}
}
}
Note There is no real need for this fixed-width formatter to derive from MediaTypeFormatter; you can equally well derive from BufferedMediaTypeFormatter. Here I derive from MediaTypeFormatter and use the asynchronous methods with await only for the purpose of illustration. Using asynchronous methods for CPU-bound operations has no benefit and creates only overhead.
Listing 3-14. Adding the Formatter to the Collection
config.Formatters.Add(
new FixedWidthTextMediaFormatter());
Request |
GET http://localhost:55778/api/employees HTTP/1.1 Host: localhost:55778 Accept: text/plain |
Response |
HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Date: Wed, 03 Apr 2013 05:39:17 GMT Content-Length: 144 012345John Human 012346Jane Public 012347Joseph Law |
config.Formatters.Add(new FixedWidthTextMediaFormatter());
Listing 3-15. Adding Media Type Mapping
var fwtMediaFormatter = new FixedWidthTextMediaFormatter();
fwtMediaFormatter.MediaTypeMappings.Add(
new QueryStringMapping("frmt", "fwt",
new MediaTypeHeaderValue("text/plain")));
config.Formatters.Add(fwtMediaFormatter);
3.9 Extending an Out-of-Box Media Formatter
In this exercise you will piggyback on an out-of-box media formatter, in this case JsonMediaTypeFormatter, and extend its functionality. JavaScript Object Notation (JSON), as the name indicates, is based on the JavaScript language for representing objects. For example, consider the following JSON:
{"Id":12345,"FirstName":"John","LastName":"Human"}
It is nothing but the JSON representation of the resource, which is an employee of ID 12345. By wrapping the preceding JSON with a function call around it—that is, by padding it—we can have JSON interpreted as object literals in JavaScript. For example, by wrapping the preceding JSON with a function named callback, we can have the payload evaluated as JavaScript, as shown in Listing 3-16. If you copy-and-paste this code into a view of an ASP.NET MVC application, for example, and navigate to the corresponding URI, an alert box with the data from JSON is displayed.
Listing 3-16. Using Padded JSON
@section scripts{
<script type="text/javascript">
$(document).ready(function () {
callback( { "Id": 12345, "FirstName": "Johny", "LastName": "Human" });
});
callback = function (employee) {
alert(employee.Id + ' ' + employee.FirstName + ' ' + employee.LastName);
};
</script>
}
This technique is used to get around the restriction imposed by browsers called the same-origin policy. This policy allows JavaScript running on the web page originating from a site (defined by a combination of scheme, hostname, and port number) to access the methods and properties of another page originating from the same site but prevents access to pages originating from different sites. For example, the URI for an employee resource that we have been using all along is http://localhost:55778/api/employees/12345. If you try to access this from the JavaScript running in a page from another ASP.NET MVC application, say http://localhost:30744/Home/Index, the browser will not allow the call. This is in accordance with the same-origin policy.
One of the ways to get around this restriction is to make use of the leniency shown towards <script> tags to get the script content from anywhere. Consider the following JavaScript tag:
<script type="text/javascript" src=" http://localhost:55778/api/employees/12345 "></script>
This can be used from http://localhost:30744/Home/Index. JSON will be retrieved, but the problem is that the downloaded JSON can only be evaluated as a JavaScript block. To interpret the data as object literals, a variable assignment is needed, and because we wrap a function call around it and have the function already defined in the /Home/Index view, the data becomes a JavaScript literal and the function with the same name as that of the wrapping function can access the data. That is exactly what I showed you in Listing 3-14.
Now, I’ll show you the steps by which browsers enforce the same-origin policy, so you’ll understand how we can get around the restriction by using JSONP. Most importantly, I’ll show you how to extend JsonMediaTypeFormatter, the formatter responsible for producing the JSON, to produce JSONP. Remember that ASP.NET Web API can only produce JSON out of the box. For this purpose, we do not write a custom formatter from scratch. We just extend the existing one because we need to only create the wrapping. The actual JSON payload generation is something we do not want to worry about, and so we let JsonMediaTypeFormatter take care of that.
The objective of this exercise is only to demonstrate subclassing an out-of-box formatter, not to solve the same-origin policy restriction. That policy is there for security reasons. You will not want to allow the script executing in a page to which you have browsed to access pages from sites you do not trust or will never go to. When you must work around the restriction, there are better techniques available, such as Cross-Origin Resource Sharing (CORS). I have covered CORS in another Apress book, Pro ASP.NET Web API Security (Apress, 2013). Also, there is a great resource available in the form of Thinktecture.IdentityModel that supports CORS. In fact, that will be part of ASP.NET Web API VNext. At the time of writing of this book, this functionality is available in the System.Web.Cors namespace in the nightly builds.
Listing 3-17. The Home/Index View
@section scripts{
<script type="text/javascript">
$(document).ready(function () {
$('#search').click(function () {
$('#employee').empty();
$.getJSON(" http://localhost:55778/api/employees/12345 ", function (employee) {
var content = employee.Id + ' ' + employee.FirstName + ' ' + employee.LastName;
$('#employee').append($('<li/>', { text: content }));
});
});
});
</script>
}
<div>
<div>
<h1>Employees Listing</h1>
<input id="search" type="button" value="Get" />
</div>
<div>
<ul id="employee" />
</div>
</div>
Listing 3-18. The JsonpMediaTypeFormatter Class (Incomplete)
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Web;
public class JsonpMediaTypeFormatter : JsonMediaTypeFormatter
{
private const string JAVASCRIPT_MIME = "application/javascript";
private string queryStringParameterName = "callback";
private string Callback { get; set; }
private bool IsJsonp { get; set; }
public JsonpMediaTypeFormatter()
{
// Do not want to inherit supported media types or
// media type mappings of JSON
SupportedMediaTypes.Clear();
MediaTypeMappings.Clear();
// We have our own!
SupportedMediaTypes.Add(new MediaTypeHeaderValue(JAVASCRIPT_MIME));
MediaTypeMappings.Add(new QueryStringMapping(
"frmt", "jsonp", JAVASCRIPT_MIME));
}
// other members go here
}
Listing 3-19. The CanReadType Method
public override bool CanReadType(Type type)
{
return false;
}
Listing 3-20. The GetPerRequestFormatterInstance Method
public override MediaTypeFormatter GetPerRequestFormatterInstance(
Type type,
HttpRequestMessage request,
MediaTypeHeaderValue mediaType)
{
bool isGet = request != null && request.Method == HttpMethod.Get;
string callback = String.Empty;
if (request.RequestUri != null)
{
callback = HttpUtility.ParseQueryString(
request.RequestUri.Query)
[queryStringParameterName];
}
// Only if this is an HTTP GET and there is a callback do we consider
// the request a valid JSONP request and service it. If not,
// fallback to JSON
bool isJsonp = isGet && !String.IsNullOrEmpty(callback);
// Returning a new instance since callback must be stored at the
// class level for WriteToStreamAsync to output. Our formatter is not
// stateless, unlike the out-of-box formatters.
return new JsonpMediaTypeFormatter() { Callback = callback, IsJsonp = isJsonp };
}
Listing 3-21. The SetDefaultContentHeaders Method
public override void SetDefaultContentHeaders(Type type,
HttpContentHeaders headers,
MediaTypeHeaderValue mediaType)
{
base.SetDefaultContentHeaders(type, headers, mediaType);
if (!this.IsJsonp)
{
// Fallback to JSON content type
headers.ContentType = DefaultMediaType;
// If the encodings supported by us include the charset of the
// authoritative media type passed to us, we can take that as the charset
// for encoding the output stream. If not, pick the first one from
// the encodings we support.
if (this.SupportedEncodings.Any(e => e.WebName.Equals(mediaType.CharSet,
StringComparison.OrdinalIgnoreCase)))
headers.ContentType.CharSet = mediaType.CharSet;
else
headers.ContentType.CharSet = this.SupportedEncodings.First().WebName;
}
}
Listing 3-22. The WriteToStreamAsync Method
public override async Task WriteToStreamAsync(Type type, object value,
Stream stream,
HttpContent content,
TransportContext transportContext)
{
using (stream)
{
if (this.IsJsonp) // JSONP
{
Encoding encoding = Encoding.GetEncoding
(content.Headers.ContentType.CharSet);
using (var writer = new StreamWriter(stream, encoding))
{
writer.Write(this.Callback + "(");
await writer.FlushAsync();
await base.WriteToStreamAsync(type, value, stream, content,
transportContext);
writer.Write(")");
await writer.FlushAsync();
}
}
else // fallback to JSON
{
await base.WriteToStreamAsync(type, value, stream, content,
transportContext);
return;
}
}
}
config.Formatters.Add(new JsonpMediaTypeFormatter());
ACCEPT HEADER AND AJAX (XMLHTTPREQUEST)
The query string media type mapping will not be needed for all browsers. For example, I use Internet Explorer 9.0.8112. When compatibility view is disabled, it correctly sends the Accept header in the request as Accept: application/javascript, */*;q=0.8. By sending application/javascript, it makes sure our formatter of JsonpMediaTypeFormatter is chosen by DefaultContentNegotiator.
Run Internet Explorer and go to http://localhost:30744. Now press F12. In the Developer Tools, select the Network tab and choose Start Capturing ➤ Get. In the capture, go to detailed view and select the Request Headers tab to see the Accept header. I covered the F12 Developer Tools in Chapter 2.
In browsers that do not send the required Accept header, frmt=jsonp must be sent in the query string. When you enable compatibility view with Internet Explorer, it starts sending Accept: */* so that DefaultContentNegotiator will choose JsonMediaTypeFormatter (without a p) by default. By passing frmt=jsonp in the query string, we ensure that our formatter is chosen regardless of the Accept header.
3.10 Controlling Which Members Are Serialized
By default, JsonMediaTypeFormatter and XmlMediaTypeFormatter use the Json.NET library and DataContractSerializer class, respectively, to perform serialization.
To prevent a property or field from being serialized, apply the IgnoreDataMember attribute. This works with both Json.NET and DataContractSerializer. To have only Json.NET ignore, apply the JsonIgnore attribute, as shown in Listing 3-23. To use IgnoreDataMember, add a reference to the System.Runtime.Serialization assembly.
Listing 3-23. The Employee Class with Json.NET Attributes
using System;
using System.Runtime.Serialization;
using Newtonsoft.Json;
public class Employee
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public decimal Compensation
{
get
{
return 5000.00M;
}
}
[JsonIgnore] // Ignored only by Json.NET
public string Title { get; set; }
[IgnoreDataMember] // Ignored by both Json.NET and DCS
public string Department { get; set; }
}
To prevent all the members from being serialized by default, apply the DataContract attribute at the class level. Then apply the DataMember attribute to only those members (including the private ones) that you want to be serialized. This approach works with both Json.NET and DataContractSerializer. See Listing 3-24.
Listing 3-24. The Employee Class with DataContract
[DataContract]
public class Employee
{
[DataMember]
public int Id { get; set; }
public string FirstName { get; set; } // Does not get serialized
[DataMember]
public string LastName { get; set; }
[DataMember]
public decimal Compensation
{
// Serialized with json.NET but fails with an exception in case of
// DataContractSerializer, since set method is absent
get
{
return 5000.00M;
}
}
}
3.11 Controlling How Members Are Serialized
ASP.NET Web API uses Json.NET and DataContractSerializer for serializing CLR objects into JSON and XML, respectively. For XML, you can use XMLSerializer instead of DataContractSerializer by setting the UseXmlSerializer property to true, as shown in the following line of code:
config.Formatters.XmlFormatter.UseXmlSerializer = true;
XMLSerializer gives you more control over the resulting XML. This is important if you must generate the XML in accordance with an existing schema. DataContractSerializer is comparatively faster and can handle more types but gives you less control over the resulting XML.
Json.NET and DataContractSerializer (specifically XMLSerializer) both have lots of knobs and switches to control the serialization output. I cover only a small subset here. You will need to refer to the respective documentation for more information.
3.11.1 Controlling Member Names
By default, the names of the members are used as-is while creating the serialized representation. For example, a property with name LastName and value of Human gets serialized as <LastName>Human</LastName> in case of XML and "LastName":"Human" in case of JSON. It is possible to change the names. In the case of Json.NET, we do this using JsonProperty with a PropertyName, as shown in Listing 3-25. In the case of DataContractSerializer, DataMember can be used but will have no effect unless DataContract is used at the class level, which forces you to apply the DataMember attribute for all the individual members.
Listing 3-25. The Employee Class with Member Names Customized for Serialization
public class Employee
{
[JsonProperty(PropertyName="Identifier")]
public int Id { get; set; }
public string FirstName { get; set; }
[DataMember(Name="FamilyName")] // No effect unless DataContract used
public string LastName { get; set; }
}
3.11.2 Prettifying JSON
In C#, the general coding standard is to use Pascal-casing for property names. In JavaScript and hence JSON, the standard is camel-casing. You can retain the C# standards and yet have the JSON camel-cased. It is also possible to get the JSON indented. Add the code shown in Listing 3-26 to the Register method of WebApiConfig in the App_Start folder.
Listing 3-26. Camel-Casing and Indenting JSON
config.Formatters.JsonFormatter
.SerializerSettings.Formatting = Formatting.Indented;
config.Formatters.JsonFormatter
.SerializerSettings.ContractResolver = new
CamelCasePropertyNamesContractResolver();
With this, if you make a GET to http://localhost:55778/api/employees, the resulting JSON is well-formatted!
[
{
"id": 12345,
"firstName": "John",
"lastName": "Human"
},
{
"id": 12346,
"firstName": "Jane",
"lastName": "Public"
},
{
"id": 12347,
"firstName": "Joseph",
"lastName": "Law"
}
]
3.12 Returning Only a Subset of Members
Often you’ll need to return only a subset of the properties of a class; this exercise shows how to do that. Take the case of the Employee class shown in Listing 3-27.
Listing 3-27. The Employee Class with Five Properties
public class Employee
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public decimal Compensation { get; set; }
public int Department { get; set; }
}
Suppose you need to return only two properties, say Id and a new property called Name, which is nothing but FirstName and LastName concatenated. One option is to create a new type and then create and return instances of that type. Another option, which I show here, is to use anonymous types.
One of the great features of C# is the ability to create new types on the fly using anonymous types. They are essentially compiler-generated types that are not explicitly declared. Anonymous types typically are used in the select clause of a query expression to return a subset of the properties from each object in the source sequence.
To try anonymous types for yourself, create a new ApiController, as shown in Listing 3-28.
Listing 3-28. Employees Controller Returning Anonymous Type
public class EmployeesController : ApiController
{
private static IList<Employee> list = new List<Employee>()
{
new Employee()
{
Id = 12345, FirstName = "John", LastName = "Human"
},
new Employee()
{
Id = 12346, FirstName = "Jane", LastName = "Public"
},
new Employee()
{
Id = 12347, FirstName = "Joseph", LastName = "Law"
}
};
public HttpResponseMessage Get()
{
var values = list.Select(e => new
{
Identifier = e.Id,
Name = e.FirstName + " " + e.LastName
});
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ObjectContent(values.GetType(),
values,
Configuration.Formatters.JsonFormatter)
};
return response;
}
}
This code explicitly returns ObjectContent from the Get action method, using the anonymous type that we create in the select clause. The important point to note here is that XmlFormatter cannot handle anonymous types. We pass the JsonFormatter while creating the ObjectContent instance and make sure conneg result is not used and any formatter other than JsonFormatter is not picked for serialization. Here is the JSON output:
[
{
"Identifier": 12345,
"Name": "John Human"
},
{
"Identifier": 12346,
"Name": "Jane Public"
},
{
"Identifier": 12347,
"Name": "Joseph Law"
}
]
Summary
From the perspective of ASP.NET Web API, serialization is the process of translating a .NET Common Language Runtime (CLR) type into a format that can be transmitted over HTTP. The format is either JSON or XML, out of the box. A media type formatter, which is an object of type MediaTypeFormatter, performs the serialization in the ASP.NET Web API pipeline. The out-of-box media formatters that produce JSON and XML are respectively JsonMediaTypeFormatter and XmlMediaTypeFormatter, both deriving from MediaTypeFormatter.
It is possible to create your own media formatter to handle media types other than JSON and XML. To create a media formatter, you must derive from the MediaTypeFormatter class or the BufferedMediaTypeFormatter class. The BufferedMediaTypeFormatter class also derives from the MediaTypeFormatter class, but it wraps the asynchronous read and write methods inside synchronous blocking methods.
The process through which the MediaTypeFormatter is chosen is called Content Negotiation. The System.Net.Http.Formatting.DefaultContentNegotiator class implements the default content negotiation algorithm in the Negotiate method that it implements, as part of implementing the IContentNegotiatior interface. In the world of HTTP, a resource can have one or more representations. The Web API indicates how a resource is represented in the response through the Content-Type response header. The Accept request header can be used by a client to indicate the set of preferred representations for the resource in the response. The Accept request header and media type mappings are important in the process of content negotiation.
ASP.NET Web API uses Json.NET and DataContractSerializer for serializing CLR objects into JSON and XML respectively. For XML, you can opt for XMLSerializer instead of DataContractSerializer by setting the UseXmlSerializer property to true. XMLSerializer gives you more control over how you want the resulting XML to be. This is important if you must generate the XML in accordance with an existing schema. DataContractSerializer is comparatively faster and can handle more types but gives you less control over the resulting XML.