CHAPTER 12

image

Creating Media Type Formatters

Media type formatters are the component responsible for serializing model data so that it can be sent to the client. In this chapter, I explain how media type formatters work by creating a custom data format and using it to explain the different ways in which a formatter can be applied. Table 12-1 summarizes this chapter.

Table 12-1. Chapter Summary

Problem

Solution

Listing

Create a media type formatter.

Derive from the MediaTypeFormatter class and implement the CanReadType, CanWriteType, and WriteToStreamAsync methods.

1–3

Register a media type formatter.

Add an instance of the custom class to the formatter collection during Web API configuration.

4

Consume a media type formatter in the client.

Use the dataType and accepts settings to configure the Ajax request.

5

Add support for content encoding.

Use the SupportedEncodings collection to define character encodings.

6

Set headers on the responses generated by the media type formatters.

Override the SetDefaultContentHeaders method.

7

Allow a media type formatter to participate in the content negotiation process.

Create media type mappings or use the media type mapping extension methods.

8–9, 11

Add headers to client HTTP requests.

Use the headers setting to configure the Ajax request.

10

Create a new instance of the media type formatter class for each request.

Override the GetPerRequestFormatterInstance method.

12

Image Note  Web API includes built-in media type formatters that generate JSON and XML data; I explain how these work and how they can be configured in Chapter 13. Media type formatters are also used to deserialize data as part of the model binding process, which I explain in Chapter 14.

Preparing the Example Project

I am going to continue working with the ExampleApp project I created in Chapter 10 and added to in Chapter 11. In preparation for this chapter, I am going to tidy up the code in the Product controller to use the conventional mechanism for producing results. Listing 12-1 shows the revised controller, from which I have removed the NoOp action method and changed the results of the GetAll and Delete methods.

Listing 12-1. Changes to the ProductsController.cs File

using System.Collections.Generic;
using System.Web.Http;
using ExampleApp.Models;

namespace ExampleApp.Controllers {
    public class ProductsController : ApiController {
        IRepository repo;

        public ProductsController(IRepository repoImpl) {
            repo = repoImpl;
        }

        public IEnumerable<Product> GetAll() {
            return repo.Products;
        }

        public void Delete(int id) {
            repo.DeleteProduct(id);
        }
    }
}

Image Tip  Remember that you don’t have to create the example project yourself. You can download the source code for every chapter for free from Apress.com.

I want to disable the custom content negotiator class I created in Chapter 11 so that I can demonstrate the interaction between the default implementation and the media type formatter classes. Listing 12-2 shows the change I made to the AddBindings method of the NinjectResolver class.

Listing 12-2. Disabling a Mapping in the NinjectResolver.cs File

...
private void AddBindings(IKernel kernel) {
    kernel.Bind<IRepository>().To<Repository>().InSingletonScope();
    // kernel.Bind<IContentNegotiator>().To<CustomNegotiator>();
}
...

To make sure that the default content negotiator is being used, start the application and use the browser to request the /api/products URL. The default negotiator will return XML content. If you see JSON, then you have forgotten to comment out the statement in the WebApiConfig.cs file, as described in Chapter 11.

Creating a Media Type Formatter

The best way to understand how media type formatters work is to create one, which is done by deriving from the abstract MediaTypeFormatter class defined in the System.Net.Http.Formatting namespace. In the sections that follow, I describe different aspects of implementing a media type formatter that supports a custom data format. My formatter will serialize Product objects and will do so by generating a set of comma-separated values for the properties defined by the Product class in the following order: ProductID, Name, Price. The effect will mean that while a JSON representation of the data in the repository looks like this:

[{"ProductID":1,"Name":"Kayak","Price":275.0},
 {"ProductID":2,"Name":"Lifejacket","Price":48.95},
 {"ProductID":3,"Name":"Soccer Ball","Price":19.50},
 {"ProductID":4,"Name":"Thinking Cap","Price":16.0}]

My custom format will serialize the same data like this:

1,Kayak,275.0,2,Lifejacket,48.95,3,Soccer Ball,19.50,4,Thinking Cap,16.0

My custom data format is can be used only to represent Product objects, which allows me to demonstrate some important characteristics of media type formatting. I need to pick a MIME type so that I can set the Accept request header and Content-Type response header. I will use the following:

application/x.product

This MIME type will allow the content negotiator to select my custom media type formatter, as I explained in Chapter 11.

Image Tip  MIME types are expressed in the form <type>/<subtype>, and prefixing the subtype with x. indicates a private content type. The MIME type specification—RFC 6838—discourages the use of private content types, but they remain useful for custom data formats and are still widely used. Older versions of the standard allowed a x- prefix, which is no longer supported. See http://tools.ietf.org/html/rfc6838#section-3.4 for details.

Table 12-2 puts custom media type formatters into context.

Table 12-2. Putting Custom Media Type Formatters in Context

Question

Answer

What are they

Media type formatters are responsible for serializing model data so that it can be sent to the client (and reversing the process as part of the model binding process that I describe in Chapter 14).

When should you use them?

There are built-in formatters for the JSON and XML formats, which I describe in Chapter 13. Custom formatters are required for other data formats.

What do you need to know?

Media type formatters can alter the way that content is serialized based on different aspects of the request, including request headers and character encodings. Media type formatters can also take an active role in content negotiation, as described in the “Participating in the Negotiation Process” section.

Implementing a Basic Media Type Formatter

To demonstrate how to create a custom media type formatter, I added a class file called ProductFormatter.cs to the Infrastructure folder of the example project and used it to define the class shown in Listing 12-3.

Listing 12-3. The Contents of the ProductFormatter.cs File

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.Threading.Tasks;
using ExampleApp.Models;

namespace ExampleApp.Infrastructure {
    public class ProductFormatter : MediaTypeFormatter {

        public ProductFormatter() {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x.product"));
        }

        public override bool CanReadType(Type type) {
            return false;
        }

        public override bool CanWriteType(Type type) {
            return type == typeof(Product) || type == typeof(IEnumerable<Product>);
        }

        public override async Task WriteToStreamAsync(Type type, object value,
                Stream writeStream, HttpContent content,
                TransportContext transportContext) {

            List<string> productStrings = new List<string>();
            IEnumerable<Product> products = value is Product
                ? new Product[] { (Product)value } : (IEnumerable<Product>)value;

            foreach (Product product in products) {
                productStrings.Add(string.Format("{0},{1},{2}",
                    product.ProductID, product.Name, product.Price));
            }

            StreamWriter writer = new StreamWriter(writeStream);
            await writer.WriteAsync(string.Join(",", productStrings));
            writer.Flush();
        }
    }
}

The MediaTypeFormatter class defines a SupportedMediaTypes collection, which is used by the content negotiator to match MIME types in the client Accept header to a formatter. When creating a custom formatter, you add instances of the MediaTypeHeaderValue class to the SupportedMediaTypes collection to list the content types that the formatter can serialize, like this:

...
public ProductFormatter() {
    SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x.product"));
}
...

The constructor argument for the MediaTypeHeaderValue class is a MIME type, and I have specified the private content type I will be using.

Indicating Type Support

There are only two methods that custom media type formatters must implement because they are marked as abstract by the base class: CanReadType and CanWriteType. Media type formatters can use these methods to restrict the range of data types that they operate on, which makes it easy to create narrowly focused formatters that have explicit knowledge of the classes they will serialize.

The CanReadType method is used as part of the model binding process, which I describe in Chapter 14. The CanWriteType method is called by the content negotiator to see whether the formatter is able to serialize a specific type. It is important that you return true from the CanWriteType for all permutations of data object you want to serialize. The Web API Product controller in the example application has an action method that returns IEnumerable<Product>, and I have added support for the Product type on its own (not in an array or enumeration) so I can support action methods that return a single Product object, as follows:

...
public override bool CanWriteType(Type type) {
    return type == typeof(Product) || type == typeof(IEnumerable<Product>);
}
...

Serializing Model Data

Setting the supported MIME types and implementing the CanWriteType method provide the content negotiator with the information it needs to determine whether the formatter is able to deal with a request. The WriteToStreamAsync method is where the real work happens and is called when the content negotiator has selected the formatter for serializing the model objects returned by the action method. The WriteToStreamAsync method accepts the argument types described in Table 12-3.

Table 12-3. The Argument Types Accepted by the WriteToStreamAsync Method

Argument Type

Description

Type

The type of the model data as returned by the action method.

object

The data to serialize.

Stream

The stream to which the serialized data should be written. You must not close the stream.

HttpContent

A context object that provides access to the response headers. You must not modify this object.

TransportContext

A context object that provides information about the network transport, which can be null.

The WriteToStreamAsync method is asynchronous, and it returns a Task that will serialize the data objects to the stream, optionally using the HttpContent object to get information about the response that will be sent. The HttpContent object provides access to the headers for the response through a Headers property, which I use in the “Supporting Content Encodings” section later in the chapter.

One of the benefits of creating narrowly focused formatters that deal with just a small number of types is that they are simple to implement. The WriteToStreamAsync method in the ProductFormatter class returns a Task that creates a string for each Product object it receives, joins them together with commas, and writes the combined result to the stream.

...
public override async Task WriteToStreamAsync(Type type, object value,
        Stream writeStream, HttpContent content,
        TransportContext transportContext) {

    List<string> productStrings = new List<string>();
    IEnumerable<Product> products = value is Product
        ? new Product[] { (Product)value } : (IEnumerable<Product>)value;

    foreach (Product product in products) {
        productStrings.Add(string.Format("{0},{1},{2}",
            product.ProductID, product.Name, product.Price));
    }

    StreamWriter writer = new StreamWriter(writeStream);
    await writer.WriteAsync(string.Join(",", productStrings));
    writer.Flush();
}
...

Image Note  Although the WriteToStreamAsync method is asynchronous, there is an alternative base class, BufferedMediaTypeFormatter, which you can use if you prefer to work synchronously and are willing to accept that request handling threads may block while the formatter performs its serialization. I recommend you take the time to write asynchronous implementations because the BufferedMediaTypeFormatter class just provides a synchronous wrapper around the asynchronous methods defined by the MediaTypeFormatter class anyway.

Registering the Media Type Formatter

The set of media type formatter classes is accessed through the HttpConfiguration.Formatters property, which returns an instance of the System.Net.Http.MediaTypeFormatterCollection class. The MediaTypeFormatterCollection class defines the methods I have listed in Table 12-4 for manipulating the collection of formatters, as well as some convenience properties for working with the built-in formatters that I describe in Chapter 11.

Table 12-4. The Methods Defined by the MediaTypeFormattingCollection for Manipulating the Collection

Name

Description

Add(formatter)

Adds a new formatter to the collection

Insert(index, formatter)

Inserts a formatter at the specified index

Remove(formatter)

Removes the specified formatter

RemoveAt(index)

Removes the formatter at the specified index

Listing 12-4 shows how I have used the Add method to register my ProductFormatter class with Web API in the WebApiConfig.cs file.

Listing 12-4. Registering a Media Type Formatter in the WebApiConfig.cs File

using System.Web.Http;
using ExampleApp.Infrastructure;

namespace ExampleApp {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            config.DependencyResolver = new NinjectResolver();

            //config.Services.Replace(typeof(IContentNegotiator),
            //  new CustomNegotiator());

            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            config.Formatters.Add(new ProductFormatter());
        }
    }
}

Using the Custom Formatter

Testing the custom formatter is easy with Postman. Click the Headers button and add an Accept header with a value of application/x.product. (Postman provides a helpful list of HTTP headers to aid your selection.) Set the verb to GET and set the URL so that it targets the /api/products URL on your local machine, using the port that Visual Studio assigned to the example project. Start the application and then click the Postman Send button. The content negotiator will use the Accept header in the request and the type of the object returned by the action method to select the ProductFormatter media type formatter, producing the result shown in Figure 12-1.

9781484200865_Fig12-01.jpg

Figure 12-1. Testing the custom formatter with Postman

As the figure shows, the result from the request matches my target output, shown here:

1,Kayak,275.0,2,Lifejacket,48.95,3,Soccer Ball,19.50,4,Thinking Cap,16.0

Consuming the Formatted Data with jQuery

jQuery makes it easy to target custom formatters by setting the Accept header in Ajax requests, although using a custom data format means that the data returned by the web service won’t be automatically converted into JavaScript objects like it is for JSON. Listing 12-5 shows the changes I made to the exampleApp.js file to specify the application/x.product MIME type and process the data that the formatter generates.

Listing 12-5. Consuming a Custom Data Format in the exampleApp.js File

$(document).ready(function () {

    deleteProduct = function (data) {
        $.ajax("/api/products/" + data.ProductID, {
            type: "DELETE",
            success: function () {
                products.remove(data);
            }
        })
    };

    getProducts = function() {
        $.ajax("/api/products", {
            dataType: "text",
            accepts: {
                text: "application/x.product"
            },
            success: function (data) {
                products.removeAll();
                var arr = data.split(",");
                for (var i = 0; i < arr.length; i += 3) {
                    products.push({
                    ProductID: arr[i],
                        Name: arr[i + 1],
                        Price: arr[i + 2]
                    });
                }
            }
        })
    };
    ko.applyBindings();
});

Two jQuery Ajax settings are required to configure the Accept heading. The dataType setting tells jQuery how to process the data that will be received from the web service. The value of text means that plain text is expected and should not be processed by jQuery the way that other formats such as JSON are. The accepts (note the plural: accepts and not accept) setting tells jQuery which MIME type should be used for the data format specified by the dataType setting. It is a convoluted technique, but it works and has the effect of setting the Accept header in the HTTP request to application/x.product.

When I receive the data in the success callback function, I use the split method to break up the string into an array and then process the array items to create JavaScript objects that I add to the Knockout observable array. You can test the changes by starting the application and clicking the Refresh button, using the browser F12 tools to inspect the resulting HTTP request and response.

Refining the Custom Formatter

Now that I have the basic functionality in place, I can use some of the more advanced formatter features to refine the way that the formatter is matched to requests and the serialized data produced by the formatter.

Supporting Content Encodings

The Accept header is the main mechanism by which formatters are selected to serialize data, but clients can express a preference about the character encodings they want to receive by using the Accept-Charset request header.

Image Tip  If you are not familiar with text encoding, then see the useful Wikipedia article at http://en.wikipedia.org/wiki/Character_encoding for an introduction.

Testing the Accept-Charset header can be difficult because the standard that describes how Ajax requests are made prohibits some headers from being set explicitly, including the Accept-Charset header, and this means there is no way to set this header using jQuery.

In fact, the only reliable way to test the effect of different values for the Accept-Charset header is through the Interceptor add-on for Postman, which overrides the default behavior enforced by the browser and allows all headers to be set. I explained how to install Interceptor in Chapter 1, and you will need to follow these instructions before testing the code in this section.

The MediaTypeFormatter class defines a SupportedEncodings property, which returns a Collection<System.Text.Encoding> object that custom formatters can populate with details of the encodings they support. By default, formatters are assumed to support all encodings, but in Listing 12-6, I have added a statement to the constructor of the ProductFormatter class that restricts the formatter to specific encodings.

Listing 12-6. Supporting a Specific Encoding in the ProductFormatter.cs File

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.Threading.Tasks;
using ExampleApp.Models;
using System.Text;

namespace ExampleApp.Infrastructure {
    public class ProductFormatter : MediaTypeFormatter {

        public ProductFormatter() {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x.product"));
            SupportedEncodings.Add(Encoding.Unicode);
            SupportedEncodings.Add(Encoding.UTF8);
        }

        public override bool CanReadType(Type type) {
            return false;
        }

        public override bool CanWriteType(Type type) {
            return type == typeof(Product) || type == typeof(IEnumerable<Product>);
        }

        public override async Task WriteToStreamAsync(Type type, object value,
                Stream writeStream, HttpContent content,
                TransportContext transportContext) {

            List<string> productStrings = new List<string>();
            IEnumerable<Product> products = value is Product
                ? new Product[] { (Product)value } : (IEnumerable<Product>)value;

            foreach (Product product in products) {
                productStrings.Add(string.Format("{0},{1},{2}",
                    product.ProductID, product.Name, product.Price));
            }

            Encoding enc = SelectCharacterEncoding(content.Headers);
            StreamWriter writer = new StreamWriter(writeStream, enc ?? Encoding.Unicode);
            await writer.WriteAsync(string.Join(",", productStrings));
            writer.Flush();
        }
    }
}

The System.Text.Encoding class defines static properties for widely used encodings, and the additions I made to the constructor add the UTF-16 (accessed through the Unicode property) and UTF-8 encodings to the SupportedEncodings collection.

Image Tip  The HTML5 specification recommends using the UTF-8 encoding for all web content. See https://www.w3.org/International/questions/qa-choosing-encodings for more details.

In the WriteToStreamAsync method, I call the SelectCharacterEncoding methods defined by the base class, pass in the value of the HttpContent.Headers property, and receive the Encoding that should be used for the content—or null if there is no content encoding that matches the client preferences. The final step is to set the encoding on the StreamWriter object that I create to serialize the data.

...
StreamWriter writer = new StreamWriter(writeStream, enc ?? Encoding.Unicode);
...

Image Tip  The way that the content encoding is selected is a little odd. The Accept and Accept-Charset request headings are used to create the Content-Type response header before the formatter is asked to render the content. If there is a match between the encodings requested by the client and those supported by the formatter, the Content-Type header will include the encoding, like this: application/x.product; charset=utf-16. The SelectCharacterEncoding method then parses the Content-Type header to figure out which encoding should be used. This is awkward—and it has the feel of trying to shoehorn a feature into the formatter without having access to the request context object.

Testing the character encoding support requires the following steps in Postman:

  1. Set the URL so that it targets /api/products on your local machine.
  2. Set the verb to GET.
  3. Click the Interceptor button on the menu bar (the one that looks like a stoplight) so that it turns green. (If you can’t find the Interceptor button, it is likely that you forgot to install the extension. See Chapter 1 for instructions.) Click the Header button and add an Accept header with a value of application/x.product and an Accept-Charset header with a value of utf-16.
  4. Click the Send button.

Click the Headers tab below the Send button once the request has completed to see the response headers. The model data in the example application doesn’t contain any characters that require a specific encoding, but you can see the effect of the changes I made by looking at the Content-Length and Content-Type headers, which areas follows:

Content-Length: 138
Content-Type: application/x.product; charset=utf-16

The Content-Length headers reports that the response is 138 bytes, and the Content-Type header reports that the data the response contains is of the application/x.product type, encoded with utf-16.

Next, change the value of the Accept-Charset request header to utf-8 and click the Send button again. You will see the following headers in the response:

Content-Length: 71
Content-Type: application/x.product; charset=utf-8

The size of the response is smaller because the UTF-8 encoding uses fewer bits to encode each character. Finally, set the Accept-Charset request header to utf-32, and click the Send button again to produce the following response headers:

Content-Length: 138
Content-Type: application/x.product; charset=utf-16

The specification for the Accept-Charset header allows two outcomes when there is no overlap between the encodings requested by the client and those supported by the web service. The first option is to send a 406 (Not Acceptable) response. The second option—which is the one that Web API uses—is to use any encoding and hope that the client can make some sense of it. The encoding that is used is the first one in the SupportedEncodings collection, which is utf-16 for the ProductFormatter class.

Setting the HTTP Response Headers

Web API sets the HTTP response headers based on the media type and character encoding that have been selected. You can change the headers that are added to the response by overriding the SetDefaultContentHeaders method and either set different headers or supplement the ones defined by the base class. Listing 12-7 shows how I have added a new header to the HTTP responses for which the ProductFormatter class serializes data.

Listing 12-7. Setting the HTTP Response Headers in the ProductFormatter.cs File

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.Threading.Tasks;
using ExampleApp.Models;
using System.Text;

namespace ExampleApp.Infrastructure {
    public class ProductFormatter : MediaTypeFormatter {

        public ProductFormatter() {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x.product"));
            SupportedEncodings.Add(Encoding.Unicode);
            SupportedEncodings.Add(Encoding.UTF8);
        }

        public override bool CanReadType(Type type) {
            return false;
        }

        public override bool CanWriteType(Type type) {
            return type == typeof(Product) || type == typeof(IEnumerable<Product>);
        }

        public override void SetDefaultContentHeaders(Type type,
            HttpContentHeaders headers, MediaTypeHeaderValue mediaType) {
            base.SetDefaultContentHeaders(type, headers, mediaType);
            headers.Add("X-ModelType",
                type == typeof(IEnumerable<Product>)
                    ? "IEnumerable<Product>" : "Product");
            headers.Add("X-MediaType", mediaType.MediaType);
        }

        public override async Task WriteToStreamAsync(Type type, object value,
                Stream writeStream, HttpContent content,
                TransportContext transportContext) {

            List<string> productStrings = new List<string>();
            IEnumerable<Product> products = value is Product
                ? new Product[] { (Product)value } : (IEnumerable<Product>)value;

            foreach (Product product in products) {
                productStrings.Add(string.Format("{0},{1},{2}",
                    product.ProductID, product.Name, product.Price));
            }

            Encoding enc = SelectCharacterEncoding(content.Headers);
            StreamWriter writer = new StreamWriter(writeStream, enc ?? Encoding.Unicode);
            await writer.WriteAsync(string.Join(",", productStrings));
            writer.Flush();
        }
    }
}

The SetDefaultContentHeaders method is passed the type that will be serialized, an HttpContentHeaders object that is used to create new headers, and a MediaTypeHeaderValue object that contains details of the MIME type and character encoding that have been selected by the content negotiator.

I have called the base implementation of the method to set the Content-Type header and used the method arguments to add two nonstandard headers to the response (headers whose names start with X- are nonstandard). The HttpContentHeaders class defines methods that allow headers to be defined, as described in Table 12-5.

Table 12-5. The Methods Defined by the HttpContentHeaders Class

Name

Description

Add(header, value)

Adds a new header to the response with the specified value

Remove(header)

Removes a header from the response

Image Tip  The HttpContentHeaders class also defines a number of convenience properties that get common header values. I have not listed them in the table because they are not used by media type formatters, which are focused on setting, rather than reading, header values.

I call the Add method to define the X-ModelType header, which I set to a human-readable representation of the model type that the formatter will serialize, as follows:

...
headers.Add("X-ModelType", type == typeof(IEnumerable<Product>)
    ? "IEnumerable<Product>" : "Product");
...

The other header I added relies on the MediaTypeHeaderValue object, which provides details of the media type and encoding that the negotiator selected through the properties shown in Table 12-6. (This is the same MediaTypeHeaderValue class that I used to express the MIME types that the formatter supports in Listing 12-3.)

Table 12-6. The Methods Defined by the MediaTypeHeaderValue Class

Name

Description

CharSet

Gets or sets the character encoding, expressed as a string

MediaType

Gets or sets the MIME type, expressed as a string

I used the MediaType property to set the value of the X-MediaType header, as follows:

...
headers.Add("X-MediaType", mediaType.MediaType);
...

These nonstandard response headers don’t affect the way that the client processed the data, but they can be useful for debugging. To test the changes that I made in Listing 12-7, start the application and use Postman to send a GET request to the /api/products URL with an Accept header of application/x.product. The headers shown by Postman will include the X-ModelType and X-MediaType headers, like this:

Cache-Control: no-cache
Content-Length: 138
Content-Type: application/x.product; charset=utf-16
Date: Thu, 27 Mar 2014 17:41:15 GMT
Expires: -1
Pragma: no-cache
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-MediaType: application/x.product
X-ModelType: IEnumerable<Product>
X-Powered-By: ASP.NET
X-SourceFiles=?UTF-8?B?QzpcVXNlcnN...

Notice that my nonstandard headers are not the only ones in the response: ASP.NET adds several headers for diagnostics purposes.

Participating in the Negotiation Process

The basic negotiation process I described in Chapter 11 relies on the content negotiator doing all of the work, examining the Accept header sent by the client and matching it to one of the MIME types that the formatters have declared support for.

Formatters can take a more active role in the negotiation process by defining one or more implementations of the abstract MediaTypeMapping class, which is used to decide how the MIME types supported by the formatter fit into the client preferences for each request. Table 12-7 puts media type mappings into context.

Table 12-7. Putting Media Type Mappings in Context

Question

Answer

What is it?

Media type formatters can participate in the content negotiation process by inspecting the request and overriding the Accept header sent by the client.

When should you use it?

Use this feature to extend the negotiation process beyond the Accept header, which can be useful when working with widely used but badly implementing clients (such as legacy browsers).

What do you need to know?

Use this feature sparingly so that you don’t send a format to the client that it can’t understand.

Creating a Media Type Mapping

As a demonstration, I added a class file called ProductMediaMapping.cs to the Infrastructure folder and used it to define the class shown in Listing 12-8.

Listing 12-8. The Contents of the ProductMediaMapping.cs File

using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Formatting;

namespace ExampleApp.Infrastructure {

    public class ProductMediaMapping : MediaTypeMapping {

        public ProductMediaMapping()
            : base("application/x.product") {
        }

        public override double TryMatchMediaType(HttpRequestMessage request) {
            IEnumerable<string> values;
            return request.Headers.TryGetValues("X-UseProductFormat", out values)
                && values.Where(x => x == "true").Count() > 0 ? 1 : 0;
        }
    }
}

The MediaTypeMapping class defines a constructor that accepts the MIME type that the mapping relates to. The TryMatchMediaType method is passed the HttpRequestMessage object that represents the current request and is responsible for returning a double value that indicates the client preference for the specified MIME type.

The double has the same effect as the q values in the Accept header sent by the client. The MediaTypeMapping class provides a mechanism by which formatters can override the preferences expressed by the client and promote or demote their formats in the list of matches. There are no constraints on which details of the request are used to make the decision. My example looks for an X-UseProductFormat header in the request. If the header is true, then I return a value of 1, indicating that the client has a strong preference for the application/x.product format. If the header isn’t included in the request or isn’t set to true, then I return 0 to indicate that the client does not want to accept the data format. Listing 12-9 shows how I have applied the ProductMediaMapping to the constructor of the custom formatter.

Listing 12-9. Using a MediaTypeMapping in the ProductFormatter.cs File

...
public ProductFormatter() {
    //SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x.product"));
    SupportedEncodings.Add(Encoding.Unicode);
    SupportedEncodings.Add(Encoding.UTF8);
    MediaTypeMappings.Add(new ProductMediaMapping());
}
...

Image Caution  Use this feature sparingly. Clients expect their format preferences to be managed through the Accept header, and you can create problems by overriding this behavior.

I have commented out the call to the SupportedMediaTypes.Add method to prevent the formatter from participating passively in the negotiation process and added a call to the MediaTypeMappings.Add method to register an instance of the ProductMediaMapping class. The MediaTypeMappings property returns a collection of MediaTypeMapping objects, and a formatter can register as many mappings as it requires.

Testing the Negotiation Process

The best way to test the effect of the mapping is with Postman because it makes it easy to control the headers. Send a GET request to the /api/products URL with the headers and values shown in Table 12-8.

Table 12-8. The Request Headers and Values Required to Test the MediaTypeMapping Implementation

Header

Value

Accept

application/json;q=0.9

X-UseProductFormat

true

The Accept header is set so that the client expresses a 0.9 preference for the application/json format, which will be overridden by the 1.0 preference that the ProductMediaMapping class will report for the application/x.product format because the request contains the X-UseProductFormat header, as shown in Figure 12-2. If you remove the X-UseProductFormat header and send another request, the web service will honor the Accept header and send JSON data.

9781484200865_Fig12-02.jpg

Figure 12-2. Overriding client format preferences

Adding Headers to jQuery Ajax Requests

Adding headers to jQuery Ajax requests is simple, as shown in Listing 12-10.

Listing 12-10. Adding a Nonstandard Request Header in the exampleApp.js File

$(document).ready(function () {

    deleteProduct = function (data) {
        $.ajax("/api/products/" + data.ProductID, {
            type: "DELETE",
            success: function () {
                products.remove(data);
            }
        })
    };

    getProducts = function() {
        $.ajax("/api/products", {
            headers: { "X-UseProductFormat": "true" },
            dataType: "text",
            accepts: {
                text: "application/x.product"
            },
            success: function (data) {
                products.removeAll();
                var arr = data.split(",");
                for (var i = 0; i < arr.length; i += 3) {
                    products.push({
                        ProductID: arr[i],
                        Name: arr[i + 1],
                        Price: arr[i + 2]
                    });
                }
            }
        })
    };
    ko.applyBindings();
});

Image Caution  The code in the listing assumes that you live in a locale that doesn’t use commas to represent fractional amounts.

The headers setting is set to an object whose properties correspond to the headers that will be added to the request. This change allows the client to continue to receive the application/x.product format, even though there is no longer a static mapping for the media type formatter.

Using the Mapping Extension Methods

Deriving from the MediaTypeMapping class allows you to dig right into the details of the request as part of the negotiation process, but Web API also provides some convenient extension methods that make it easy to set up the most common mappings. Table 12-9 describes the extension methods, all of which are applied to MediaTypeFormatter objects.

Table 12-9. The Extension Methods for Mapping Media Types to Requests

Method

Description

AddQueryStringMapping(name, value, mimeType)

Selects the specified mimeType when the request query string contains the name property with the specified value.

AddRequestHeaderMapping(name, value,comparison, substring, mimeType)

Selects the specified mimeType when the request contains a name header with the specified value. The comparison argument is a System.StringComparison value used to compare the request value, which will accept substrings is the substring argument is true.

AddUriPathExtensionMapping(extension, mimeType)

Selects the specified mimeType if the request URL has the specified extension.

These extension methods can be used during the registration of a media type formatter, as shown in Listing 12-11.

Listing 12-11. Using Media Type Formatter Mapping Methods in the WebApiConfig.cs File

using System.Web.Http;
using ExampleApp.Infrastructure;
using System.Net.Http.Formatting;
using System;

namespace ExampleApp {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            config.DependencyResolver = new NinjectResolver();

            //config.Services.Replace(typeof(IContentNegotiator),
            //  new CustomNegotiator());

            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "Api with extension",
                routeTemplate: "api/{controller}.{ext}/{id}",
                defaults: new { id = RouteParameter.Optional,
                    ext = RouteParameter.Optional }
            );

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            MediaTypeFormatter prodFormatter = new ProductFormatter();
            prodFormatter.AddQueryStringMapping("format", "product",
                "application/x.product");
            prodFormatter.AddRequestHeaderMapping("X-UseProductFormat", "true",
                StringComparison.InvariantCultureIgnoreCase, false,
                "application/x.product");
            prodFormatter.AddUriPathExtensionMapping("custom", "application/x.product");
            config.Formatters.Add(prodFormatter);
        }
    }
}

The AddQueryStringMapping extension method gives preference to a media type formatter when a query string contains a specific property and value. I used this method in the listing so that the ProductFormatter class will be selected when the request contains a query string property called format that is set to product, like this:

...
prodFormatter.AddQueryStringMapping("format", "product", "application/x.product");
...

You can test the effect by using Postman to send a GET request to the /api/products?format=product URL. A URL that doesn’t include the format property or that has a different value won’t be affected.

I used the AddRequestHeaderMapping extension method to achieve the same effect I created with the ProductMediaMapping class (although defining your own mapping classes provides a wider range of customization options).

...
prodFormatter.AddRequestHeaderMapping("X-UseProductFormat", "true",
    StringComparison.InvariantCultureIgnoreCase, false, "application/x.product");
...

Requests that contain the X-UseProductFormat header with a case-insensitive value of true will select the ProductFormatter class. This statement is redundant in the example because the ProductFormatter class is already configured to support this header.

The AddUriPathExtensionMapping method is a little more complex than the others and requires a URL route to be defined. This method registers a mapping that looks for a routing segment variable called ext, which is the convention for capturing file extensions but which can be used to match any URL segment. I explain how Web API routes work in Chapters 20 and 21, but here is the route that I defined that captures the ext segment:

...
config.Routes.MapHttpRoute(
    name: "Api with extension",
    routeTemplate: "api/{controller}.{ext}/{id}",
    defaults: new { id = RouteParameter.Optional, ext = RouteParameter.Optional }
);
...

I used the AddUriPathExtensionMapping method so that the ProductFormatter class will be selected when the value of the ext segment variable is custom. You can test this mapping by using Postman to send a GET request to /api/products.custom.

Creating Per-Request Media Type Formatters

A single instance of a media type formatter class is usually used to serialize data for multiple requests, but an alternative approach is to override the GetPerRequestFormatterInstance method defined by the MediaTypeFormatter class. Table 12-10 puts per-request media type formatters into context.

Table 12-10. Putting Per-Request Media Type Formatters in Context

Question

Answer

What is it?

Per-request formatters allow the nature of individual requests to be used to influence the way that data is serialized and allow code that it not thread-safe to be integrated into Web API.

When should you use it?

You don’t often have to tailor serialized data based on the request, and this feature is most often used to integrate legacy serialization code into Web API so that the application can support clients from older applications.

What do you need to know?

This feature is simple to use, but remember that a new instance of the formatter class is created for each request for which the formatter is selected by the negotiation process.

Creating the Formatter Instance

The GetPerRequestFormatterInstance method is passed the Type of the data that is to be serialized, the HttpRequestMessage that represents the current request, and a MediaTypeHeaderValue that provides details of the required MIME type and character set encoding. The result of the GetPerRequestFormatterInstance method is a MediaTypeFormatter object that will be used for a single request. This feature is useful when you need to adapt the data serialization based on the individual requests or when dealing with code that is not thread-safe and that cannot afford to have its WriteToStreamAsync method called concurrently. Listing 12-12 shows how I have overridden the GetPerRequestFormatterInstance method in the ProductFormatter class to include details from the request in the serialized data.

Listing 12-12. Creating Per-Request Media Type Formatters in the ProductFormatter.cs File

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.Threading.Tasks;
using ExampleApp.Models;
using System.Text;

namespace ExampleApp.Infrastructure {
    public class ProductFormatter : MediaTypeFormatter {
        private string controllerName;

        public ProductFormatter() {
            //SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x.product"));s
            SupportedEncodings.Add(Encoding.Unicode);
            SupportedEncodings.Add(Encoding.UTF8);
            MediaTypeMappings.Add(new ProductMediaMapping());
        }

        public ProductFormatter(string controllerArg) : this() {
            controllerName = controllerArg;
        }

        public override bool CanReadType(Type type) {
            return false;
        }

        public override bool CanWriteType(Type type) {
            return type == typeof(Product) || type == typeof(IEnumerable<Product>);
        }

        public override void SetDefaultContentHeaders(Type type,
            HttpContentHeaders headers, MediaTypeHeaderValue mediaType) {
            base.SetDefaultContentHeaders(type, headers, mediaType);
            headers.Add("X-ModelType",
                type == typeof(IEnumerable<Product>)
                    ? "IEnumerable<Product>" : "Product");
            headers.Add("X-MediaType", mediaType.MediaType);
        }

        public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type,
                HttpRequestMessage request, MediaTypeHeaderValue mediaType) {
            return new ProductFormatter(
                request.GetRouteData().Values["controller"].ToString());
        }

        public override async Task WriteToStreamAsync(Type type, object value,
                Stream writeStream, HttpContent content,
                TransportContext transportContext) {

            List<string> productStrings = new List<string>();
            IEnumerable<Product> products = value is Product
                ? new Product[] { (Product)value } : (IEnumerable<Product>)value;

            foreach (Product product in products) {
                productStrings.Add(string.Format("{0},{1},{2}",
                    product.ProductID,
                    controllerName == null ? product.Name :
                    string.Format("{0} ({1})", product.Name, controllerName),
                    product.Price));
            }

            Encoding enc = SelectCharacterEncoding(content.Headers);
            StreamWriter writer = new StreamWriter(writeStream, enc ?? Encoding.Unicode);
            await writer.WriteAsync(string.Join(",", productStrings));
            writer.Flush();
        }
    }
}

I have modified the ProductFormatter class so that it includes the name of the controller to which the routing system has matched the request. I explain how Web API routing works in Chapters 20 and 21, but the key point for this chapter is that each request may be handled by a different controller, so I need to use the GetPerRequestFormatterInstance method so that I have access to the HttpRequestMessage object to get the information about the request that I need.

Testing the Per-Request Formatter

To test the changes, start the application and click the Refresh button in the browser window that Visual Studio opens. The jQuery client code sends the nonstandard request header required to match the request to the ProductFormatter class, which adds products to the name of the serialized data since that is the name of the controller that handles the request (and, of course, the only Web API controller in the application, but you get the idea). Figure 12-3 shows the effect.

9781484200865_Fig12-03.jpg

Figure 12-3. Including per-request information in serialized data

Image Tip  If you don’t get the expected result, then right-click the Chrome Refresh button and select Empty Cache and Hard Reload from the pop-up window. This option is available only when the F12 developer tools window is opened, and it ensures that Chrome requests the most recent version of the JavaScript file from the server.

Summary

In this chapter, I showed you how to create and use media type formatters to serialize model data so that it can be sent to the web service client. I explained how formatters fit within Web API, demonstrated how to support different media types and character encodings, and demonstrated how to create formatters that are able to participate in the content negotiation process. In the next chapter, I show you how to work with the built-in media type formatters, which are responsible for producing JSON and XML data.

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

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