CHAPTER 3

image

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.

  1. You can use the project from Exercise 1.2 or create a new ASP.NET MVC 4 project with a name of HelloWebApi using Web API template.
  2. If you create a new project, add the Employee class from the project corresponding to Exercise 1.2 into your new project under the Models folder. The following code listing shows the Employee class, for your easy reference.
    public class Employee
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Department { get; set; }
    }
  3. Modify the Register method in WebApiConfig in the App_Start folder, as shown in Listing 3-1, to see the output it produces (also shown in Listing 3-1). You can see this output in the Output window of Visual Studio, as you press F5 and run the application.

    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:

  1. The type of the object to serialize
  2. The collection of media formatters
  3. The request object (HttpRequestMessage)

The Negotiate method checks the following four items before deciding on the media formatter to use, in descending order of precedence:

  1. Media type mapping: Every MediaTypeFormatter has a collection of MediaTypeMapping values. A MediaTypeMapping allows you to map the request or response messages that have certain characteristics to a media-type. There are four out-of-box media type mappings: QueryStringMapping, UriPathExtensionMapping, RequestHeaderMapping, and MediaRangeMapping. These respectively map a query string parameter, URI path extension, request header, and media range to a media type. As an example, defining a QueryStringMapping with a parameter name of fmt and a value of json and media-type of application/json will let ASP.NET Web API choose JsonMediaTypeFormatter, if the query string has a field fmt with a value of json, such as this: http://localhost:<port>/api/employees/12345?fmt=json.
  2. Media type as specified in the Accept request header.
  3. Media type as specified in the Content-Type request header.
  4. If there is no match so far, the conneg algorithm goes through the MediaTypeFormatter objects defined in the config and checks if a formatter can serialize the type by calling the CanWriteType method. The first formatter that can serialize the type is chosen.

Try the following steps to see for yourself how ASP.NET Web API conneg works.

  1. You can use the project from Exercise 1.2 or create a new ASP.NET MVC 4 project (Web API template). If it does not already exist, add a new ApiController with a name of EmployeesController and implement an action-method–handling GET, as shown in Listing 3-2. Also copy the Employee class into the Models folder of your new project.

    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);
        }
    }
  2. Fire up Fiddler and go to the Composer tab. Issue a GET request for http://localhost:55778/api/employees/12345, specifying Accept: application/json in the Request Headers text box. Remember to replace the port 55778 with the port that the application runs on. The Web API response you get will be JSON.

    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"}
  3. Issue a GET request for http://localhost:55778/api/employees/12345, specifying Accept: application/xml in the Request Headers text box. Now, the Web API response is XML.

    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>
  4. Issue a GET request for http://localhost:55778/api/employees/12345, specifying Accept: application/xml;q=0.2, application/json;q=0.8 in the Request Headers text box. The Web API response is JSON, since application/json has the quality factor of 0.8, which is greater than 0.2 for XML.

    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
    ...
  5. Issue a GET request for http://localhost:55778/api/employees/12345, specifying Content-Type: application/xml in the Request Headers text box. Do not include the Accept header. The Web API response is XML. Even though there is no message body for the request, we specified a Content-Type. Since there is no Accept header for conneg to choose the media type, it resorted to Content-Type.

    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
    ...
  6. Issue a GET request for http://localhost:55778/api/employees/12345, without specifying either the Accept header or the Content-Type header. The Web API response is JSON. Because conneg cannot determine the media type based on the Accept header or the Content-Type header, it just goes through the list of the MediaTypeFormatter objects, in the same way that we looped in Listing 3-1. The order in which the MediaTypeFormatter objects are listed in Listing 3-1 is significant because it determines the order in which ASP.NET Web API picks up the default formatter to serialize. The first media type formatter in the list is JsonMediaTypeFormatter. Since this media type formatter can serialize the Employee type (notice the true returned by CanWriteType in Listing 3-1), the Web API chooses it and responds with JSON.

    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
    ...
  7. Modify the WebApiConfig class in the App_Start folder to add the statement
    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));
            }
        }
    }
  8. Issue a GET request for http://localhost:55778/api/employees/12345 without specifying either the Accept or the Content-Type header. Now ASP.NET Web API sends back an XML response by default, since JsonMediaTypeFormatter is no longer first in the list; instead, XmlMediaTypeFormatter is now first. If you repeat the GET explicitly asking for JSON with the Accept: application/json header, even then you will get only the XML representation of the Employee object.

    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
    ...
  9. In WebApiConfig in the App_Start folder, immediately after the code you added in the previous step, add the same code again: config.Formatters.RemoveAt(0);. This removes the second formatter from the Formatters collection, which is XmlMediaTypeFormatter.
  10. Issue a GET request for http://localhost:55778/api/employees/12345. The Web API now responds with a 406 - Not Acceptable status code. By removing both the formatters, we have left the API with no formatter option to serialize Employee type and hence the 406 status code.

    Request GET http://localhost:55778/api/employees/12345 HTTP/1.1
    Host: localhost:55778
    Response HTTP/1.1 406 Not Acceptable
    Content-Length: 0
  11. Modify the WebApiConfig class to delete both the lines we added to remove the first formatter from the collection:
    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.

  1. Make a change to the Register method of WebApiConfig in the App_Start folder, as shown in Listing 3-4.

    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));

            }
     
        }
    }
  2. We have now mapped the query string of field frmt with a value of json to the media type application/json and xml to the media type application/xml.
  3. Issue a GET request for http://localhost:55778/api/employees/12345 ?frmt=json specifying Accept: application/xml in the Request Headers text box. As always, remember to replace the port 55778 with the actual port that the application runs on. Pay attention to the query string that is part of the URI. The Web API response is JSON, even though we have specified application/xml in the Accept header. The conneg algorithm has chosen application/json based on the query string media type mapping, which takes precedence over the Accept header.

    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"}
  4. Issue a GET request for http://localhost:55778/api/employees/12345 ?frmt=xml, specifying Accept: application/json in the Request Headers text box. The response will be XML this time. Whether or not the Accept header is present, the response is always XML because of the order of precedence.

    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.

  1. Add the code shown in Listing 3-5 to the Register method of WebApiConfig in the App_Start folder, following the lines we added in Exercise 3.3.

    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")));
  2. Issue a GET request for http://localhost:55778/api/employees/12345, specifying two request headers, Accept: application/xml and X-Media: json, in the Request Headers text box. The Web API response is JSON, even though we have specified application/xml in the Accept header. The conneg algorithm has chosen application/json based on the header media type mapping, which takes precedence over the Accept header.

    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"}
  3. Issue a GET request for http://localhost:55778/api/employees/12345 ?frmt=xml, specifying X-Media: json in the Request Headers text box. The response is still JSON.

    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.

  1. Add a reference to the System.ServiceModel assembly to your project by right-clicking References under your project in Visual Studio Solution Explorer and selecting Add Reference.
  2. Add a new class IPBasedMediaTypeMapping, as shown in Listing 3-6.

    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;
        }
    }
  3. Add it to the media type mappings collection of the JSON media type formatter in the Register method of WebApiConfig in the App_Start folder, as shown in Listing 3-7.

    Listing 3-7.  Registering the Custom Media Type Mapping

    config.Formatters.JsonFormatter
                .MediaTypeMappings.Add(new IPBasedMediaTypeMapping());
  4. Issue a GET request using Fiddler, from the machine running Web API, for http://localhost:55778/api/employees/12345, specifying Accept: application/xml in the Request Headers text box. Remember to replace the port 55778 with the actual port that the application runs on. The Web API response is JSON, even though we have specified application/xml in the Accept header. The conneg algorithm has chosen application/json based on the IP address that is mapped to application/json.

    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"}
  5. I have chosen an IP address only for illustration. You can create the mapping to literally anything on the request, including headers, and you can implement any complex logic that is based on the multiple parameters of the request to choose the media type.
  6. Undo all the changes we have made so far to the WebApiConfig class and restore it to the out-of-box state, as shown in Listing 3-8.

    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.

  1. Modify the GET action method in EmployeesController, as shown in Listing 3-9.

    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)
        };
    }
  2. Rebuild the solution and make a GET request to http://localhost:55778/api/employees/12345. Regardless of the Accept and Content-Type headers, you will always get JSON back.

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>.

  1. You can use an existing ASP.NET Web API project or create a new one.
  2. If it does not already exist, create a new model class Employee, as shown in Listing 3-11.

    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; }
    }
  3. Create a new ApiController with the name EmployeesController and add the action method shown in Listing 3-10 earlier. In the example I use some hard-coded data to return, for the purpose of illustration. If you use the EmployeesController class from an existing project, ensure that the controller class has the static list and the Get action method, as shown in Listing 3-12.

    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;
        }
    }
  4. Create a new class FixedWidthTextMediaFormatter, deriving from MediaTypeFormatter, as shown in Listing 3-13.
    • a.   In the constructor, add UTF-8 and UTF-16 to the SupportedEncodings collection. By doing so, you can support two charsets for the clients to choose from.
    • b.   Add the media type of text/plain to the SupportedMediaTypes collection. This will let conneg pick our formatter when a client asks for this media type.
    • c.   We will not support requests coming in as fixed-width text for this exercise, so return false in the CanReadType method.
    • d.   For serialization, we support only a list of employees (IEnumerable<Employee>). You will check for this list in the CanWriteType method.
    • e.   The WriteToStreamAsync method does the actual serialization by formatting a string in accordance with the width specifications for the fields. Call the SelectCharacterEncoding method from the MediaTypeFormatter base class to get the most appropriate encoding and use it to create the StreamWriter.

    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();
                    }
                }
            }
        }
    }
     

    image 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.

  5. Register the formatter in the Register method of WebApiConfig in the App_Start folder, as shown in Listing 3-14.

    Listing 3-14.  Adding the Formatter to the Collection

    config.Formatters.Add(
                        new FixedWidthTextMediaFormatter());
  6. Rebuild the solution and make a GET request to http://localhost:55778/api/employees from the Composer tab of Fiddler. Remember to include Accept: text/plain, to indicate to the Web API that you would like the fixed-width format.

    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
  7. Make a GET to http://localhost:55778/api/employees from Internet Explorer by typing the URI in the address bar. The content downloaded is JSON. The reason for this behavior is that IE specified Accept: text/html, application/xhtml+xml, */*. Because of the */*, ASP.NET Web API picks up the first formatter in the collection that can serialize IEnumerable<Employee>. This happens to be JsonMediaTypeFormatter and not our custom formatter, which we added to the end of the formatters collection.
  8. It is possible to conneg based on query string, as we saw in Exercise 3.3. In the preceding steps, we simply added the formatter into the formatter collection. It is also possible to specify media type mapping and then add it to the Formatters collection. Comment out the line you added to the WebApiConfig class:
    config.Formatters.Add(new FixedWidthTextMediaFormatter());
    
  9. Add the code shown in Listing 3-15 to the Register method of WebApiConfig in the App_Start folder.

    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);
  10. With this change, if you request a GET for http://localhost:55778/api/employees?frmt=fwt from Internet Explorer, ASP.NET Web API will return the response in text/plain. This technique of using query string media type mapping will be especially handy when the client does not have the ability to add the Accept header to the request message.

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.

  1. As with other exercises, you can create a new ASP.NET Web API project and implement an action method to handle GET or use the project from Exercise 1.2.
  2. I assume the URI for the employee resource with ID of 12345 is http://localhost:55778/api/employees/12345. If you run in a different port, you will need to adjust the port number.
  3. Create a new ASP.NET MVC project in the same solution. The name does not matter, so use any name of your liking, say TestMvcApplication. You can choose the Web API template or other MVC templates as well. We just need an MVC controller. Ensure that Razor is selected as the view engine when you create the new project.
  4. Go to the Home/Index view and replace the generated code with the code shown in Listing 3-17. Remember to replace the port 55778 with the actual port that your ASP.NET Web API application runs on.

    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>
  5. In the Solution Explorer of Visual Studio, right-click the TestMvcApplication project and select Debug Start New Instance. This will open up Internet Explorer. Assuming your MVC application runs on port 30744, the URI will be http://localhost:30744.
  6. If you click Get, nothing happens; the Ajax call does not go through. The browser is enforcing the same-origin policy. The page that is part of the domain localhost:30744 is not allowed to access a resource in localhost:55778.
  7. Now that we are on this view, change the URI used by getJSON to http://localhost:55778/api/employees/12345?frmt=jsonp&callback =?. It will not work yet either, but as we go through the remaining steps of this exercise, we will get it working.
  8. Create a new class JsonpMediaTypeFormatter, deriving from JsonMediaTypeFormatter, as shown in Listing 3-18, in the ASP.NET Web API project. The media type that the formatter will support is application/javascript. The name of the wrapper function will be made available to us in the request as a query string parameter with a name of callback. In the code I’ve made sure the media types and media type mappings from the base class are not inherited, by clearing out these collections. I do not intend this formatter to handle application/json. I leave that to the out-of-box JsonMediaTypeFormatter. I add a new media type query string mapping so that a client can explicitly ask for JSONP. This should explain why we changed the URI in the previous step to http://localhost:55778/api/employees/12345?frmt=jsonp&callback =?. We do not supply the wrapping function name, because jQuery will do that dynamically at run time. We only need to have a placeholder in the form of a question mark.

    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
    }
  9. Override the CanReadType method and return false to indicate that our formatter cannot be used for deserialization of any types, as shown in Listing 3-19.

    Listing 3-19.  The CanReadType Method

    public override bool CanReadType(Type type)
    {
                return false;
    }
  10. Override the GetPerRequestFormatterInstance method, as shown in Listing 3-20. The DefaultContentNegotiator calls this method after it selects a formatter. This method gives us the opportunity to inspect the HttpRequestMessage object. It checks for two things: (1) the HTTP method and (2) the name of the wrapping function passed by jQuery in the query string. The code wraps the JSON response with this callback function name. Here is the most important point. Since we need the callback function to be available to the other two methods of the class, we must store the callback at the class level, making it stateful. The out-of-box formatters are stateless, and the same instance can handle multiple requests. Since this formatter is stateful, we return a new instance every time. Only if the HTTP method is GET and there is a callback function name available to us will the IsJsonp property of the new formatter instance be set to true.

    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 };
    }
  11. Override the SetDefaultContentHeaders method, as shown in Listing 3-21. By the time execution comes to our media formatter, the DefaultContentNegotiator has already chosen the formatter, and the media type we support (application/javascript) will be set in the Content-Type response header. However, when we must fall back to regular JSON, as when the HTTP method is not GET or the callback function is not passed, we must override this behavior and restore the Content-Type to application/json. That is exactly what this method does. DefaultMediaType corresponds to application/json, and we get this by virtue of inheritance. In addition to setting the media type, we ensure that the charset chosen by DefaultContentNegotiator is set in the Content-Type header, provided the charset is one that we support. If it is not, we choose the first of the available encodings. Note that the encoding supported by our class and the base class need not be the same. We can add a new encoding specifically for JSONP. You’ll learn more about charset encoding in Chapter 4.

    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;
        }
                    
    }
  12. Override the WriteToStreamAsync method, as shown in Listing 3-22. For JSONP, we write the callback wrapping function to the stream first and call the base class method to let it write the JSON, followed by the closing bracket. If we need to fall back to JSON, we write nothing additional to the stream and leave it fully to the base class. It’s important point here to use the correct encoding that was selected in the previous step to create the StreamWriter instance. Otherwise, what is sent back in the Content-Type header may not match how the response is encoded, especially when an encoding other than the default is picked by the DefaultContentNegotiator based on the user request.

    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;
            }
        }
    }
  13. Add the media formatter to the formatters collection in the Register method of WebApiConfig in the App_Start folder:
    config.Formatters.Add(new JsonpMediaTypeFormatter());
    
  14. Since JsonpMediaTypeFormatter does not handle application/json, there is no need to insert this as the first formatter in the collection. Because we have implemented a query string media type mapping for application/javascript, passing frmt=jsonp in the query string in addition to the callback will ensure that the DefaultContentNegotiator picks up our JSONP formatter.
  15. With all these changes in place, rebuild the solution and go to http://localhost:30744 (the MVC application's home page). Click on Get. It should now pull the employee data and display it. By making a GET to http://localhost:55778/api/employees/12345?frmt=jsonp&callback =?, we request the JSONP representation of the employee resource with ID 12345. Since it is JavaScript, the same-origin policy restrictions do not apply. jQuery’s getJSON does the rest, and we are able to read the employee data.

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.

  • The public fields and properties are serialized by default with both Json.NET and DataContractSerializer.
  • The read-only properties (properties with only the getter) are serialized by Json.NET but not by DataContractSerializer. The Compensation property of the Employee class shown in Listing 3-21 earlier is an example of this.
  • The private, protected, and internal members are not serialized in either case.

3.10.1 Blacklisting Members

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; }
}
 

3.10.2 Whitelisting Members

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.131.47