CHAPTER 4

image

Customizing Response

Request for Comments (RFC) 2616 defines content negotiation asthe process of selecting the best representation for a given response when there are multiple representations available.” RFC also states “this is not called format negotiation, because the alternate representations may be of the same media type, but use different capabilities of that type, be in different languages, etc.” The term negotiation is used because the client indicates its preferences. A client sends a list of options with a quality factor specified against each option, indicating the preference level. It is up to the service, which is Web API in our case, to fulfill the request in the way the client wants, respecting the client preferences. If Web API cannot fulfill the request the way the client has requested, it can switch to a default or send a 406 -Not Acceptable status code in the response. There are four request headers that play a major part in this process of content negotiation:

  1. Accept, which is used by a client to indicate the preferences for the media types for the resource representation in the response, such as JSON (application/json) or XML (application/xml).
  2. Accept-Charset, which is used by a client to indicate the preferences for the character sets, such as UTF-8 or UTF-16.
  3. Accept-Encoding, which is used by a client to indicate the preferences for the content encoding, such as gzip or deflate.
  4. Accept-Language, which is used by a client to indicate the preferences for the language, such as en-us or fr-fr.

Content negotiation is not just about choosing the media type for the resource representation in the response. It is also about the language, character set, and encoding. Chapter 3 covered content negotiation related to the media type, in which the Accept header plays a major role. This chapter covers content negotiation related to language, character set, and encoding.

4.1 Negotiating Character Encoding

Simply put, character encoding denotes how characters—letters, digits and other symbols—are represented as bits and bytes for storage and communication. The HTTP request header Accept-Charset can be used by a client to indicate how the response message can be encoded. ASP.NET Web API supports UTF-8 and UTF-16 out of the box. The following are the steps to see the process of character-set negotiation in action.

  1. Create a new ASP.NET MVC 4 project with a name of HelloWebApi using the Web API template.
  2. Add the Employee class, as shown in Listing 4-1, to the Models folder.

    Listing 4-1.  The Employee Class

    public class Employee
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
  3. Modify the Register method in WebApiConfig, in the App_Start folder, as shown in Listing 4-2.

    Listing 4-2.  Supported Encodings

    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 encoding in config.Formatters.JsonFormatter.SupportedEncodings)
            {
                System.Diagnostics.Trace.WriteLine(encoding.WebName);
            }
        }
    }
  4. Rebuild the solution and press F5 to run the application from Visual Studio. You will see that the code prints utf-8 followed by utf-16 in the Output window of Visual Studio. UTF-8 and UTF-16 are the character encodings supported by ASP.NET Web API out of the box. UTF-8 is the default.
  5. Add a new empty API controller with the name EmployeesController to your Web API project, as shown in Listing 4-3. You can directly copy and paste the Japanese characters into the class file and compile.

    Listing 4-3.  The EmployeesController Class

    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 = "image"
            },
                
            new Employee()
            {
                Id = 12346, FirstName = "Jane", LastName = "Public"
            },
     
            new Employee()
            {
                Id = 12347, FirstName = "Joseph", LastName = "Law"
            }
        };
     
        public Employee Get(int id)
        {
            return list.First(e => e.Id == id);
        }
    }
  6. Fire up Fiddler and send a GET request from the Composer tab to the URI http://localhost:55778/api/employees/12345. Remember to replace the port 55778 with the actual port that your application runs on.
  7. The response returned is shown in Listing 4-4. Some of the headers are removed for brevity.

    Listing 4-4.  Web API Response Showing Default Character Encoding

    HTTP/1.1 200 OK
    Content-Type: application/json; charset= utf-8
    Date: Fri, 29 Mar 2013 03:51:11 GMT
    Content-Length: 87
     
    {"Id":12345,"FirstName":"John","LastName":"image"}
  8. ASP.NET Web API has returned the content encoded in UTF-8, which is the first element in the SupportedEncodings collection that we looped through and printed the members in Listing 4-2.
  9. Change the request in the Request Headers text box as shown in Listing 4-5 and click Execute.

    Listing 4-5.  Web API Request Asking for UTF-16

    Host: localhost:55778
    Accept-charset: utf-16
  10. Web API returns the response shown in Listing 4-6. Some of the headers are removed for brevity. This time, the response is encoded in UTF-16. Because of this, the content-length has increased from 87 to 120.

    Listing 4-6.  Web API Response Encoded in UTF-16

    HTTP/1.1 200 OK
    Content-Type: application/json; charset= utf-16
    Date: Fri, 29 Mar 2013 03:52:20 GMT
    Content-Length: 120
     
    {"Id":12345,"FirstName":"John","LastName":"image"}
  11. Change the request in the Request Headers text box as shown in Listing 4-7 and click Execute.

    Listing 4-7.  Web API Request Asking for DBCS Character Encoding of Shift JIS

    Host: localhost:55778
    Accept-charset: shift_jis
  12. Web API returns the response shown in Listing 4-8. Some of the headers are removed for brevity. Since Shift JIS is not supported out of the box, ASP.NET Web API reverts to the default encoding, which is UTF-8. This is negotiation in action.

    Listing 4-8.  Web API Response When Client Requested Shift JIS

    HTTP/1.1 200 OK
    Content-Type: application/json; charset= utf-8
    Date: Fri, 29 Mar 2013 03:57:55 GMT
    Content-Length: 87
     
    {"Id":12345,"FirstName":"John","LastName":"image"}
     

4.2 Supporting DBCS Character Encoding (Shift JIS)

In this exercise, you will add support for a double-byte character set (DBCS) such as Shift JIS. The term DBCS refers to a character encoding where each character is encoded in two bytes. DBCS is typically applicable to oriental languages like Japanese, Chinese, Korean, and so on. Shift JIS (shift_JIS) is a character encoding for the Japanese language. Code page 932 is Microsoft’s extension of Shift JIS.

Why bother with DBCS like Shift JIS when Unicode is there? The answer is that there are still legacy systems out there that do not support Unicode. Also, believe it or not, there are database administrators who are not willing to create Unicode databases, and there are still old and outdated IT administration policies that prohibit creation of databases in Unicode to save storage cost, even though storage prices have fallen to such a degree that this cost-saving point becomes moot. But there are still applications out there that do not handle Unicode!

  1. Modify the Register method of the WebApiConfig class in the App_Start folder, as shown in Listing 4-9. The new line to be added is shown in bold type.

    Listing 4-9.  Enabling Shift JIS

    using System.Text;
    using System.Web.Http;
     
    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
                                    .SupportedEncodings
                                            .Add(Encoding.GetEncoding(932));
     
            foreach (var encoding in config.Formatters.JsonFormatter.SupportedEncodings)
            {
                System.Diagnostics.Trace.WriteLine(encoding.WebName);
            }
        }
    }
  2. This code adds Shift JIS support to ASP.NET Web API on a per-formatter basis; it adds Shift JIS support only to JsonFormatter. It uses the Encoding.GetEncoding method to get the Encoding object corresponding to the code page of 932, which is Shift JIS.
  3. Rebuild the solution in Visual Studio.
  4. Retry the previous Shift JIS request. Change the request in the Request Headers text box, as shown in Listing 4-7 earlier, and click Execute. The following are the headers to be copy-pasted, for your easy reference.
    Host: localhost:55778
    Accept-charset: shift_jis
  5. Web API returns the response shown in Listing 4-10. Some of the headers are removed for brevity. Now, the charset in the response is shown as shift_jis. Also, the content length is only 73 now, even less than we got with UTF-8.

    Listing 4-10.  Web API Response Encoded in Shift JIS

    HTTP/1.1 200 OK
    Content-Type: application/json; charset= shift_jis
    Date: Fri, 29 Mar 2013 04:19:36 GMT
    Content-Length: 73
     
    {"Id":12345,"FirstName":"John","LastName":"image"}
  6. Modify the static list in EmployeesController to update the last name of the employee with ID 12345 from image to Human, as shown in Listing 4-11.

    Listing 4-11.  EmployeesController Modified to Remove Japanese Characters

    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"
            }
        };
     
        // Rest of the code goes here
    }
     

4.3 Negotiating Content Encoding (Compression)

Content coding is the encoding transformation applied to an entity. It is primarily used to allow a response message to be compressed. The main objective of HTTP compression is to make better use of available bandwidth. Of course, this is achieved with a tradeoff in processing power. The HTTP response message is compressed before it is sent from the server, and the client indicates, in the request, its preference for the compression schema to be used. A client that does not support compression can opt out of it and receive an uncompressed response. The most common compression schemas are gzip and deflate. HTTP/1.1 specifies identity, which is the default encoding to denote the use of no transformation. These values are case-insensitive.

A client sends the compression schema values along with an optional quality factor value in the Accept-Encoding request header. The server (Web API) tries to satisfy the request to the best of its ability. If Web API can successfully encode the content, it indicates the compression schema in the response header Content-Encoding. Based on this, a client can decode the content. The default identity is used only in the request header of Accept-Encoding and not in the response Content-Encoding. Sending identity in Content-Encoding is same as sending nothing. In other words, the response is not encoded.

Table 4-1 shows a few sample Accept-Encoding request headers and the corresponding response details for an ASP.NET Web API that supports gzip and deflate compression schema in that order of preference.

Table 4-1. Content Coding

Accept-Encoding Content-Encoding Explanation
Accept-Encoding: gzip, deflate Gzip Both gzip and deflate default to a quality factor of 1. Since Web API prefers gzip, it will be chosen for content encoding.
Accept-Encoding: gzip;q=0.8, deflate Deflate deflate defaults to a quality factor of 1, which is greater than gzip.
Accept-Encoding: gzip, deflate;q=0 gzip The client indicates deflate must not be used but gzip can be.
Accept-Encoding: No encoding and Content-Encoding header will be absent. Per HTTP/1.1, identity has to be used, and it means no encoding.
Accept-Encoding: * gzip The client indicates that Web API can use any encoding it supports.
Accept-Encoding: identity; q=0.5, *;q=0 No encoding and Content-Encoding header will be absent. By specifying *; q=0, the client is indicating it does not like any encoding schemes. Since identity is also specified, Web API does not perform any encoding.
Accept-Encoding: zipper, * gzip The client prefers zipper, but Web API is not aware of any such scheme and does not support it. Since the client has specified the * character as well, Web API uses gzip.
Accept-Encoding: *;q=0 No encoding and Content-Encoding header will be absent. Status code will be 406 - Not Acceptable. The client is specifically refusing all schemas, and by not including identity, it has left Web API no other choice but to respond with a 406.
Accept-Encoding: DeFlAtE deflate The client is basically asking for deflate but uses a mixture of upper- and lowercase letters. Field values are case-insensitive, as per HTTP/1.1.

The following exercise demonstrates the steps involved in building a Web API that supports gzip and deflate, and negotiating with the client, as described in the preceding table.

  1. You can use the same project that you created with any of the previous exercises or you can create a new ASP.NET MVC 4 project using Web API template. I use the same project from the previous exercise.
  2. Create a new class named EncodingSchema, as shown in Listing 4-12. The field supported is a dictionary with a key the same as the supported encoding scheme. Currently, there are only two of them: gzip and deflate. The value is a Func delegate that represents the method for creating and returning the stream object: GZipStream and DeflateStream respectively for gzip and deflate. The corresponding methods are GetGZipStream and GetDeflateStream.

    Listing 4-12.  EncodingSchema (Incomplete)

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.IO.Compression;
    using System.Linq;
    using System.Net.Http.Headers;
     
    public class EncodingSchema
    {
        private const string IDENTITY = "identity";
     
        private IDictionary<string, Func<Stream, Stream>> supported =
                             new Dictionary<string, Func<Stream, Stream>>
                                               (StringComparer.OrdinalIgnoreCase);
     
        public EncodingSchema()
        {
            supported.Add("gzip", GetGZipStream);
            supported.Add("deflate", GetDeflateStream);
        }
     
        // rest of the class members go here
    }
  3. Add the two methods shown in Listing 4-13 to the class.

    Listing 4-13.  Methods to Get the Compression Streams

    public Stream GetGZipStream(Stream stream)
    {
        return new GZipStream(stream, CompressionMode.Compress, true);
    }
     
    public Stream GetDeflateStream(Stream stream)
    {
        return new DeflateStream(stream, CompressionMode.Compress, true);
    }
  4. Add another method, named GetStreamForSchema, for returning the Func delegate from the dictionary based on the schema passed in. For example, when the schema passed in is gzip, the Func<Stream, Stream> returned by this method corresponds to the GetGZipStream method that we defined in the previous step. See Listing 4-14.

    Listing 4-14.  GetStreamForSchema Method

    private Func<Stream, Stream> GetStreamForSchema(string schema)
    {
        if (supported.ContainsKey(schema))
        {
            ContentEncoding = schema.ToLowerInvariant();
            return supported[schema];
        }
     
        throw new InvalidOperationException(String.Format("Unsupported encoding schema {0}",
                                                                                      schema));

    }
  5. Add a property named ContentEncoding and another method named GetEncoder, as shown in Listing 4-15. The ContentEncoding property is set by the GetEncoder method through the private setter. For the other classes, it is a read-only property. This property returns the value to be put into the Content-Encoding response header.

    Listing 4-15.  The ContentEncoding Property and the GetEncoder Method

    public string ContentEncoding { get; private set; }
     
    public Func<Stream, Stream> GetEncoder(
                                    HttpHeaderValueCollection<StringWithQualityHeaderValue> list)
    {
            // The following steps will walk you through
            // completing the implementation of this method
    }
  6. Add the code shown in Listing 4-16 to the GetEncoder method. If the incoming list is null or has a count of 0, no processing happens and a null is returned. The incoming list is of type HttpHeaderValueCollection<StringWithQualityHeaderValue>. Each element in this collection consists of the encoding scheme along with the quality value as requested by the client in the Accept-Encoding header. For example, Accept-Encoding: gzip;q=0.8, deflate will be represented by two elements in the collection: the first element with a Value of gzip and a Quality of 0.8 and the second element with a Value of deflate and a Quality of null. Quality is a nullable decimal.

    Listing 4-16.  The GetEncoder Method

    if (list != null && list.Count > 0)
    {
            // More code goes here
    }
     
    // Settle for the default, which is no transformation whatsoever
    return null;
  7. Add the code in Listing 4-17 to the if block for a list that is not null and has Count > 0. This is the part where negotiation happens, in the following steps. The end result of this process is that the encoding scheme to be used for encoding the response message is chosen.
    • a.   Order the incoming schemes in descending order of quality value. If quality value is absent, treat it as 1.0. Choose only the schemes that have either quality value absent or present and nonzero. Match these schemes against the list of supported schemes and get the first one. If this first scheme is not null, return the corresponding scheme’s transformation function in the form of the Func delegate by calling the GetStreamForSchema method that we saw earlier. This method just returns a new Stream object corresponding to the chosen schema. Since we support only gzip and deflate, this Stream object could be either GZipStream or DeflateStream.
    • b.   If there is no match so far, see if there is a scheme of value * and quality factor of nonzero. If so, the client is willing to accept what the Web API supports. However, a client could specify a few exceptions through q=0 for specific schemes. Leave out those from the supported schemes and choose one as the scheme to use.
    • c.   If there is still no match, try to use identity. For this, check whether the client has specifically refused to accept identity, by using q=0 against it. If so, fail the negotiation by throwing NegotiationFailedException.
    • d.   As the final step, see if the client has refused all schemes through *;q=0 and has not explicitly asked for identity. In that case also, fail the negotiation by throwing NegotiationFailedException. This will send back the response status code of 406 - Not Acceptable.
    • e.   If there is no match and we do not have to throw the NegotiationFailedException so far, just skip content encoding.

      Listing 4-17.  The GetEncoder Method Continuation

      var headerValue = list.OrderByDescending(e => e.Quality ?? 1.0D)
                              .Where(e => !e.Quality.HasValue ||
                                          e.Quality.Value > 0.0D)
                              .FirstOrDefault(e => supported.Keys
                                      .Contains(e.Value, StringComparer.OrdinalIgnoreCase));
       
      // Case 1: We can support what client has asked for
      if (headerValue != null)
          return GetStreamForSchema(headerValue.Value);
       
      // Case 2: Client will accept anything we support except
      // the ones explicitly specified as not preferred by setting q=0
      if (list.Any(e => e.Value == "*" &&
              (!e.Quality.HasValue || e.Quality.Value > 0.0D)))
      {
          var encoding = supported.Keys.Where(se =>
                              !list.Any(e =>
                                          e.Value.Equals(se, StringComparison.OrdinalIgnoreCase) &&
                                              e.Quality.HasValue &&
                                                  e.Quality.Value == 0.0D))
                                                      .FirstOrDefault();
          if (encoding != null)
              return GetStreamForSchema(encoding);
      }
       
      // Case 3: Client specifically refusing identity
      if (list.Any(e => e.Value.Equals(IDENTITY, StringComparison.OrdinalIgnoreCase) &&
              e.Quality.HasValue && e.Quality.Value == 0.0D))
      {
       
          throw new NegotiationFailedException();
      }
       
      // Case 4: Client is not willing to accept any of the encodings
      // we support and is not willing to accept identity
      if (list.Any(e => e.Value == "*" &&
              (e.Quality.HasValue || e.Quality.Value == 0.0D)))
      {
          if (!list.Any(e => e.Value.Equals(IDENTITY, StringComparison.OrdinalIgnoreCase)))
              throw new NegotiationFailedException();
      }
  8. Create a new Exception class:
    public class NegotiationFailedException : ApplicationException { }.
     

    It does not carry any additional information and just derives from ApplicationException.

  9. Create a new class EncodedContent that derives from HttpContent, as shown in Listing 4-18.
    • a.   Accept an object of type HttpContent and the Func delegate in the constructor and store them in class-level fields. In the constructor, loop through the headers of the passed-in HttpContent object and add them to this instance.
    • b.   Override the TryComputeLength method and return false, since the length will not be known at the time method is called.
    • c.   Override the SerializeToStreamAsync method. Invoke the Func delegate and pass the resulting Stream object into the CopyToAsync method of the class-level field of type HttpContent.
    • d.   Note the usage of the await keyword to wait for the execution to return after the previous async call.

      Listing 4-18.  The EncodedContent Class

      using System;
      using System.IO;
      using System.Linq;
      using System.Net;
      using System.Net.Http;
      using System.Threading.Tasks;
       
      public class EncodedContent : HttpContent
      {
          private HttpContent content;
          private Func<Stream, Stream> encoder;
       
          public EncodedContent(HttpContent content, Func<Stream, Stream> encoder)
          {
              if (content != null)
              {
                  this.content = content;
                  this.encoder = encoder;
       
                  content.Headers.ToList().ForEach(x =>
                                  this.Headers.TryAddWithoutValidation(x.Key, x.Value));
              }
          }
       
          protected override bool TryComputeLength(out long length)
          {
              // Length not known at this time
              length = -1;
              return false;
          }
       
          protected async override Task SerializeToStreamAsync(Stream stream,
                                                                       TransportContext context)

          {
              using (content)
              {
                  using (Stream encodedStream = encoder(stream))
                  {
                      await content.CopyToAsync(encodedStream);
                  }
              }
          }
      }
  10. Create a new message handler named EncodingHandler, as shown in Listing 4-19. The message handler brings together the other classes we created in this exercise so far. It encodes the response and sets that as the current response content. It also adds the Content-Encoding response header. If NegotiationFailedException is thrown, it stops the processing by sending back a 406 -Not Acceptable status code.

    Listing 4-19.  The EncodingHandler

    using System.Net;
    using System.Net.Http;
    using System.Threading;
    using System.Threading.Tasks;
     
    public class EncodingHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(
                                         HttpRequestMessage request,
                                                CancellationToken cancellationToken)
        {
            var response = await base.SendAsync(request, cancellationToken);
     
            try
            {
                var schema = new EncodingSchema();
                var encoder = schema.GetEncoder(response.RequestMessage
                                                                  .Headers.AcceptEncoding);
     
                if (encoder != null)
                {
                    response.Content = new EncodedContent(response.Content, encoder);
     
                    // Add Content-Encoding response header
                    response.Content.Headers.ContentEncoding.Add(schema.ContentEncoding);
                }
            }
            catch (NegotiationFailedException)
            {
                return request.CreateResponse(HttpStatusCode.NotAcceptable);
            }
     
            return response;
        }
    }
  11. Since it is preferable to encode the content as the final step of the ASP.NET Web API pipeline, we use the message handler. Hence, it is important to configure this as the first handler so that the response processing part runs last. See Listing 4-20, where I have added the message handler to the handlers collection in WebApiConfig in the App_Start folder. Since we use the same project as the previous exercises, you see additional lines of code in WebApiConfig, but those lines do not have any bearing on the outcome of this exercise.

    Listing 4-20.  Configuring a Message Handler

    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
                                    .SupportedEncodings
                                        .Add(Encoding.GetEncoding(932));
     
            foreach (var encoding in config.Formatters.JsonFormatter.SupportedEncodings)
            {
                System.Diagnostics.Trace.WriteLine(encoding.WebName);
            }
     
            config.MessageHandlers.Add(new EncodingHandler());
            // Other handlers go here
        }
    }
  12. Rebuild the solution.
  13. Make a GET request from Fiddler for http://localhost:55778/api/employees/12345. Use different values in the Accept-Encoding header and see how Web API responds. For example, in Figure 4-1 I used Accept-Encoding: gzip;q=0.8, deflate. Since deflate has the quality factor of 1 (default), it is chosen for encoding, as shown by Fiddler.

    9781430261759_Fig04-01.jpg

    Figure 4-1. Fiddler Responding to DEFLATE Encoding

  14. Now, make a GET request to http://localhost:55778/api/employees, keeping the Accept-Encoding: gzip;q=0.8, deflate. Note the Content-Length, which is 80. This could be different for you based on what you return from the action method.
  15. Make the GET request one more time, the same as in the previous step but do not give the Accept-Encoding header. The Content-Length now is 155, since we have opted out of compression for this request. This is compression or content encoding in action.
  16. Let us now see how WebClient handles content coding. Create a console application with a name of TestConsoleApp. The Main method is shown in Listing 4-21. Remember to replace the port 55778 with the actual port that your application runs on.

    Listing 4-21.  WebClient Requesting a Content Encoded Response

    using System;
    using System.Net;
     
    class Program
    {
        static void Main(string[] args)
        {
            string uri = " http://localhost:45379/api/employees/12345 ";
     
            using (WebClient client = new WebClient())
            {
                client.Headers.Add("Accept-Encoding", "gzip, deflate;q=0.8");
                var response = client.DownloadString(uri);
     
                Console.WriteLine(response);
            }
        }
    }
  17. The preceding code prints some gibberish, as expected. We ask for compressed response but download the response as string without decompressing the bytes.

    image

  18. To make WebClient work correctly, use the code shown in Listing 4-22. Instead of using WebClient directly, this code subclasses it and overrides the GetWebRequest method to set the AutomaticDecompression property of HttpWebRequest. That will ensure the response is automatically decompressed.

    Listing 4-22.  WebClient Decompressing the Response

    class Program
    {
        static void Main(string[] args)
        {
            string uri = " http://localhost.fiddler:55778/api/employees/12345 ";
     
            using (AutoDecompressionWebClient client = new AutoDecompressionWebClient())
            {
                client.Headers.Add("Accept-Encoding", "gzip, deflate;q=0.8");
                Console.WriteLine(client.DownloadString(uri));
            }
        }
    }
     
    class AutoDecompressionWebClient : WebClient
    {
        protected override WebRequest GetWebRequest(Uri address)
        {
            HttpWebRequest request = base.GetWebRequest(address)
    as HttpWebRequest;
            request.AutomaticDecompression = DecompressionMethods.Deflate
                                                                | DecompressionMethods.GZip;
            
            return request;
        }
    }
     

4.4 Negotiating Language

The Accept-Language request header can be used by clients to indicate the set of preferred languages in the response. For example, Accept-Language: en-us, en-gb;q=0.8, en;q=0.7 indicates that the client prefers American English but when the server cannot support it, can accept British English. When that is also not supported, other types of English are also acceptable. The Accept-Language header is meant to specify the language preferences, but is commonly used to specify locale preferences as well.

4.4.1 Internationalizing the Messages to the User

In this exercise, you will internationalize the messages sent by Web API to the client. Based on the language preferences sent in the Accept-Language request header, CurrentUICulture of CurrentThread is set, and it will form the basis for language and local customization.

  1. You can use the same project that you created with the previous exercise, as I do here, or you can create a new ASP.NET MVC 4 project using Web API template. If you create a new project, make sure you have the EmployeesController and the Employee classes copied into the project.
  2. Create a new message handler, as shown in Listing 4-23. This message handler gets the language preferences from the Accept-Language request header and establishes the CurrentUICulture. As with the previous exercise, quality factor is taken into consideration while deciding on the language to be used. In this exercise, we support only two cultures: English, United States (en-us) and French, France (fr-fr).

    Listing 4-23.  CultureHandler

    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Net.Http;
    using System.Threading;
    using System.Threading.Tasks;
     
    public class CultureHandler : DelegatingHandler
    {
        private ISet<string> supportedCultures = new HashSet<string>() { "en-us", "en", "fr-fr", "fr" };
     
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
                                                         CancellationToken cancellationToken)
        {
            var list = request.Headers.AcceptLanguage;
            if (list != null && list.Count > 0)
            {
                var headerValue = list.OrderByDescending(e => e.Quality ?? 1.0D)
                                        .Where(e => !e.Quality.HasValue ||
                                                    e.Quality.Value > 0.0D)
                                        .FirstOrDefault(e => supportedCultures
                                        .Contains(e.Value, StringComparer.OrdinalIgnoreCase));
     
                // Case 1: We can support what client has asked for
                if (headerValue != null)
                {
                    Thread.CurrentThread. CurrentUICulture=
                                    CultureInfo.GetCultureInfo(headerValue.Value);
                }
     
                // Case 2: Client will accept anything we support except
                // the ones explicitly specified as not preferred by setting q=0
                if (list.Any(e => e.Value == "*" &&
                        (!e.Quality.HasValue || e.Quality.Value > 0.0D)))
                {
                    var culture = supportedCultures.Where(sc =>
                                            !list.Any(e =>
                                                    e.Value.Equals(sc, StringComparison.OrdinalIgnoreCase) &&
                                                        e.Quality.HasValue &&
                                                            e.Quality.Value == 0.0D))
                                                                .FirstOrDefault();
                    if (culture != null)
                        Thread.CurrentThread. CurrentUICulture=
                                            CultureInfo.GetCultureInfo(culture);
                }
            }
                
            return await base.SendAsync(request, cancellationToken);
        }
    }
  3. Add the handler to the Handlers collection in WebApiConfig in the App_Start folder, as shown in Listing 4-24. Since we continue to use the same project from the previous exercises, you see additional lines of code in WebApiConfig but those lines do not have any bearing on the outcome of this exercise. Nonetheless, I have commented out those lines that are not necessary for this exercise in Listing 4-24.

    Listing 4-24.  Registration of CultureHandler

    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
            //                     .SupportedEncodings
            //                          .Add(Encoding.GetEncoding(932));
     
            //foreach (var encoding in config.Formatters.JsonFormatter.SupportedEncodings)
            //{
            //    System.Diagnostics.Trace.WriteLine(encoding.WebName);
            //}
     
            //config.MessageHandlers.Add(new EncodingHandler());
     
            config.MessageHandlers.Add(new CultureHandler());
        }
    }
  4. In the Visual Studio Solution Explorer, right-click the Web API project you are working on and select Add ➤ Add ASP.NET Folder ➤ App_GlobalResources.
  5. Right click the App_GlobalResources folder created and select Add ➤ New Item. Select Resources File and give it a name of Messages.resx.
  6. Add a new string with Name of NotFound and Value of Employee you are searching for does not exist. Save the resource file.
  7. Duplicate the Message.resx by copying and pasting it into App_GlobalResourcesFolder. Rename the duplicate file Messages.fr-fr.resx.
  8. Copy and paste the text L’employé que vous recherchez n’existe pas into Value. Save the file. Pardon my French, if it is not spot on. I just used a web translator for this!
  9. Modify the Get method in EmployeesController, as shown in Listing 4-25.

    Listing 4-25.  Get Method Modified

    public Employee Get(int id)
    {
        var employee = list.FirstOrDefault(e => e.Id == id);
        if (employee == null)
        {
            var response = Request.CreateResponse(HttpStatusCode.NotFound,
                                    new HttpError(Resources.Messages.NotFound));
     
            throw new HttpResponseException(response);
        }
     
        return employee;
    }
  10. Rebuild the solution in Visual Studio.
  11. Fire-up Fiddler and make a GET to an employee resource that does not exist, for example: http://localhost:55778/api/employees/ 12399.
  12. Web API responds with a 404, as shown in Listing 4-26. Some headers are removed for brevity.

    Listing 4-26.  A 404 Response for English

    HTTP/1.1 404 Not Found
    Content-Type: application/json; charset=utf-8
    Date: Mon, 01 Apr 2013 05:48:12 GMT
    Content-Length: 59
     
    {"Message":" Employee you are searching for does not exist"}
  13. Now, make another GET request to the same URI, but this time include the request header Accept-Language: fr-fr. Web API once again responds with a 404, as shown in Listing 4-27. However, you can see that the message is in French now, in line with the language specified in the Accept-Language request header.

    Listing 4-27.  A 404 Response for French

    HTTP/1.1 404 Not Found
    Content-Type: application/json; charset=utf-8
    Date: Mon, 01 Apr 2013 05:48:02 GMT
    Content-Length: 57
     
    {"Message":" L'employé que vous recherchez n'existe pas"}
     

INTERNATIONALIZING THE RESOURCE REPRESENTATION

It is possible to internationalize the resource representation as well. For example, take the case of a product. The product description, which is part of the response content, can be internationalized. When a GET request is made to /api/products/1234, as you return a Product object, you can retrieve the description of the product based on the Thread.CurrentThread.CurrentUICulture from your persistence store. In SQL terms, this means having an additional table with a primary key of the product ID and the culture and retrieving the description from this table through a join. If you use Entity Framework as the object-relational mapper, you can let it eager-load by using Include.

4.4.2 Internationalizing the Decimal Separators of Numbers

In this exercise, you will internationalize the numbers sent to the client, specifically the decimal separator. As with the previous exercise, we use the language preferences sent in the Accept-Language header. A number (whole and fractional) has different representations in different cultures. For example, one thousand two hundred thirty four and fifty six hundredths is 1,234.56 in the US (en-us), whereas it is 1.234,56 in some European countries like France (fr-fr).

When your application has to serialize an object into a persistence store and deserialize back, you can use the invariant culture to work around this inconsistency. However, as you serialize your objects to your clients through Web API, especially when the clients are distributed around the world, there is always a need to serialize respecting the locale preferred by the client. A client can explicitly ask Web API to send the response in a locale by sending the Accept-Language header.

  1. You will use the same project that you worked with for Exercise 3.4.1. Open the project in Visual Studio.
  2. Modify the CultureHandler so that Thread.CurrentThread.CurrentCulture is also set when you set Thread.CurrentThread.CurrentUICulture. You can add the line
    Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture;
     

    immediately after the Thread.CurrentThread.CurrentUICulture is set (two places in the message handler). See Listing 4-28.

    Listing 4-28.  Setting CurrentCulture in the CultureHandler Class

    public class CultureHandler : DelegatingHandler
    {
        private ISet<string> supportedCultures = new HashSet<string>() { "en-us", "en", "fr-fr", "fr" };
     
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
                                                        CancellationToken cancellationToken)
        {
            var list = request.Headers.AcceptLanguage;
            if (list != null && list.Count > 0)
            {
                var headerValue = list.OrderByDescending(e => e.Quality ?? 1.0D)
                                        .Where(e => !e.Quality.HasValue ||
                                                    e.Quality.Value > 0.0D)
                                        .FirstOrDefault(e => supportedCultures
                                        .Contains(e.Value, StringComparer.OrdinalIgnoreCase));
     
                // Case 1: We can support what client has asked for
                if (headerValue != null)
                {
                    Thread.CurrentThread.CurrentUICulture =
                                    CultureInfo.GetCultureInfo(headerValue.Value);
     
                    Thread.CurrentThread.CurrentCulture =
                                                     Thread.CurrentThread.CurrentUICulture;
                }
     
                // Case 2: Client will accept anything we support except
                // the ones explicitly specified as not preferred by setting q=0
                if (list.Any(e => e.Value == "*" &&
                        (!e.Quality.HasValue || e.Quality.Value > 0.0D)))
                {
                    var culture = supportedCultures.Where(sc =>
                                            !list.Any(e =>
                                                    e.Value.Equals(sc,
                                                        StringComparison.OrdinalIgnoreCase) &&

                                                        e.Quality.HasValue &&
                                                            e.Quality.Value == 0.0D))
                                                                .FirstOrDefault();
                    if (culture != null)
                    {
                        Thread.CurrentThread.CurrentUICulture =
                                            CultureInfo.GetCultureInfo(culture);
     
                        Thread.CurrentThread.CurrentCulture =
                                                     Thread.CurrentThread.CurrentUICulture;
                    }
                }
            }
     
            return await base.SendAsync(request, cancellationToken);
        }
    }
  3. Add a property of type decimal with a name of Compensation to the Employee class, as shown in Listing 4-29, if this property does not exist in the model class in your project. If you have chosen to create a new project for this chapter, you will find the Compensation property missing in the Employee class.

    Listing 4-29.  New Decimal Property in the Employee Class

    public class Employee
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public decimal Compensation { get; set; }
    }
  4. Modify EmployeesController to populate a value for the new Compensation property, for employee with ID 12345, as shown in Listing 4-30.

    Listing 4-30.  Populating Compensation

    public class EmployeesController : ApiController
    {
        private static IList<Employee> list = new List<Employee>()
        {
            new Employee()
            {
                Id = 12345,
                      FirstName = "John",
                           LastName = "Human",
                                 Compensation = 45678.12M
            },
                
            new Employee()
            {
                Id = 12346, FirstName = "Jane", LastName = "Public"
            },
     
            new Employee()
            {
                Id = 12347, FirstName = "Joseph", LastName = "Law"
            }
        };
     
        // other members go here
    }
  5. Rebuild and issue a GET request to http://localhost:55778/api/employees/12345 from Fiddler, including the request header Accept-Language: fr-fr. Remember to replace the port 55778 with the actual port that your application runs on.
  6. Web API responds with the following JSON:
    {"Id":12345,"FirstName":"John","LastName":"Human","Compensation": 45678.12}
    
  7. As you can see, the Compensation property is serialized without using the culture requested by the client. JSON serialization is done by ASP.NET Web API using the JsonMediaTypeFormatter class. By default, JsonMediaTypeFormatter uses the Json.NET library, which is a third-party open source library to perform serialization. Json.NET is flexible enough for us to change this behavior.
  8. Create a new class NumberConverter that derives from JsonConverter, as shown in Listing 4-31.
    • a.   This convertor will support only decimal and nullable decimal (decimal?), as shown in the CanConvert method.
    • b.   The WriteJson method is overridden to write the value into JsonWriter, as returned by the ToString method. For this to work correctly, Thread.CurrentThread.CurrentCulture must be correctly set, which is done by our message handler that runs earlier in the ASP.NET Web API pipeline.
    • c.   The ReadJson method does the reverse. It parses the value from the reader. Though this method is not related to what we set out to achieve in this exercise, we must override this method, as part of subclassing JsonConverter.

    Listing 4-31.  NumberConverter

    using System;
    using Newtonsoft.Json;
     
    public class NumberConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return (objectType == typeof(decimal) || objectType == typeof(decimal?));
        }
     
        public override object ReadJson(JsonReader reader, Type objectType,
                                             object existingValue, JsonSerializer serializer)
        {
            return Decimal.Parse(reader.Value.ToString());
        }
     
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            writer.WriteValue(((decimal)value).ToString());
        }
    }
  9. Add NumberConverter to the list of converters used by JsonMediaTypeFormatter in WebApiConfig in the App_Start folder, as shown in Listing 4-32.

    Listing 4-32.  Adding NumberConverter to the List of Converters

    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
                    .SerializerSettings
                        .Converters.Add(new NumberConverter());
     
            //config.Formatters.JsonFormatter
            //                     .SupportedEncodings
            //                          .Add(Encoding.GetEncoding(932));
     
            //foreach (var encoding in config.Formatters.JsonFormatter.SupportedEncodings)
            //{
            //    System.Diagnostics.Trace.WriteLine(encoding.WebName);
            //}
     
            //config.MessageHandlers.Add(new EncodingHandler());
     
            config.MessageHandlers.Add(new CultureHandler());
     
        }
    }
  10. Rebuild the solution and make a GET request to http://localhost:55778/api/employees/12345 from Fiddler. Include the request header Accept-Language: fr-fr. Once again, remember to replace the port 55778 with the actual port that your application runs on.
  11. Web API now responds with this JSON:
    {"Id":12345,"FirstName":"John","LastName":"Human","Compensation":" 45678,12"}.
    
  12. We stop here with the changes we made to let Web API use the correct decimal separator. We do not proceed to format the number further on thousands separator and so on, as those aspects lean toward the formatting of the data rather than the data itself. From a Web API perspective, formatting is not relevant and is a concern of the application that presents the data to the end user.
  13. JsonMediaTypeFormatter is extended to use the NumberConverter. However, if XML resource representation is preferred by the client, the preceding steps will not be sufficient. Let us now go through the steps to handle the case of the XML formatter.
  14. Add a reference to the assembly System.Runtime.Serialization into your ASP.NET Web API project.
  15. Modify the Employee class as shown in Listing 4-33. The code uses the OnSerializing serialization callback to format the number the way we wanted. Only the fields decorated with the DataMember attribute will be serialized. We ensure that the Compensation property is not marked for serialization. Instead, we introduce a new private property and mark it for serialization under the name of Compensation. In the method marked with the OnSerializing attribute, we call ToString and set the result in the private property to be serialized into the output resource representation.

    Listing 4-33.  Using the OnSerializing Callback

    using System.Runtime.Serialization;
     
    [DataContract]
    public class Employee
    {
        [DataMember]
        public int Id { get; set; }
     
        [DataMember]
        public string FirstName { get; set; }
     
        [DataMember]
        public string LastName { get; set; }
          
        public decimal Compensation { get; set; }
     
        [DataMember(Name = "Compensation")]
        private string CompensationSerialized { get; set; }
     
        [OnSerializing]
        void OnSerializing(StreamingContext context)
        {
            this.CompensationSerialized = this.Compensation.ToString();
        }
    }
  16. With this change, rebuild the solution in Visual Studio.
  17. Make a GET request to http://localhost:55778/api/employees/12345 from Fiddler. Include the request header Accept-Language: fr-fr. Also, include another request header, Accept: application/xml.
  18. The Web API response is shown in Listing 4-34. Some headers are removed for brevity.

    Listing 4-34.  The Web API XML Response

    <Employee xmlns:i=" http://www.w3.org/2001/XMLSchema-instance "
                               xmlns=" http://schemas.datacontract.org/2004/07/HelloWebApi.Models ">
            <Compensation> 45678,12</Compensation>
            <FirstName>John</FirstName>
            <Id>12345</Id>
            <LastName>Human</LastName>
    </Employee>
     

    image Note  The serialization callback changes we made to the Employee class will get the JSON formatter working as well without the NumberConverter. You can remove the line in WebApiConfig where we add it to the converters list and test through a GET to http://localhost:55778/api/employees/12345.

  19. Finally, restore the Employee class to its original state without the DataContract or DataMember attributes, as shown in Listing 4-35.

    Listing 4-35.  The Employee Class with the OnSerializing Callback Removed

    public class Employee
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
     
        public decimal Compensation { get; set; }
    }
     

4.4.3 Internationalizing the Dates

In this exercise, you will internationalize the dates sent to the client. As with the previous exercises, we use the language preferences sent in the Accept-Language header. Unlike numbers, the date format can get really confusing to a client or an end user. For example, 06/02 could be June 02 or it could be February 06, depending on the locale.

  1. You will use the same project that you worked with for Exercise 3.4.2.
  2. Add a property to represent the employee’s date of joining, of type DateTime to Employee, as shown in Listing 4-36.

    Listing 4-36.  The Employee Class with the New Doj Property

    public class Employee
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
     
        public decimal Compensation { get; set; }
     
        public DateTime Doj { get; set; }
    }
  3. Modify EmployeesController to populate some value for the new Doj property, for the employee with ID 12345, as shown in Listing 4-37.

    Listing 4-37.  Populating Compensation

    public class EmployeesController : ApiController
    {
        private static IList<Employee> list = new List<Employee>()
        {
            new Employee()
            {
                Id = 12345,
                      FirstName = "John",
                           LastName = "Human",
                                 Compensation = 45678.12M,
                                         Doj = new DateTime(1990, 06, 02)
            },
            // other members of the list go here
        };
     
        // other class members go here
    }
  4. Rebuild and issue a GET to http://localhost:55778/api/employees/12345 from Fiddler. Include the request header Accept-Language: fr-fr. Try it with the header Accept: application/xml, as well as without it.
  5. In both cases, by default, the date is returned in the ISO 8601 format: 1990-06-02T00:00:00.
  6. To extend the JSON Media formatter, create a new class DateTimeConverter that derives from DateTimeConverterBase, as shown in Listing 4-38, in your ASP.NET Web API project.

    Listing 4-38.  DateTimeConverter

    using System;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Converters;
     
    public class DateTimeConverter : DateTimeConverterBase
    {
        public override object ReadJson(JsonReader reader, Type objectType,
                                             object existingValue, JsonSerializer serializer)
        {
            return DateTime.Parse(reader.Value.ToString());
        }
     
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            writer.WriteValue(((DateTime)value).ToString());
        }
    }
  7. Add the converter to the list of converters in WebApiConfig, in the App_Start folder. For the converter to work correctly, Thread.CurrentThread.CurrentCulture must be correctly set, which is done by the CultureHandler message handler that runs earlier in the ASP.NET Web API pipeline. Ensure that the handler is registered. Listing 4-39 shows the changes.

    Listing 4-39.  Addition of DateTimeConverter to the List of Converters

    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
                    .SerializerSettings
                        .Converters.Add(new NumberConverter());
     
            config.Formatters.JsonFormatter
                    .SerializerSettings
                        .Converters.Add(new DateTimeConverter());
     
            //config.Formatters.JsonFormatter
            //                     .SupportedEncodings
            //                          .Add(Encoding.GetEncoding(932));
     
            //foreach (var encoding in config.Formatters.JsonFormatter.SupportedEncodings)
            //{
            //    System.Diagnostics.Trace.WriteLine(encoding.WebName);
            //}
     
            //config.MessageHandlers.Add(new EncodingHandler());
     
            config.MessageHandlers.Add(new CultureHandler());
     
        }
    }
  8. Rebuild the solution in Visual Studio and issue a GET request to http://localhost:55778/api/employees/12345 from Fiddler. Include the request header Accept-Language: fr-fr. Web API returns the date as 02/06/1990 00:00:00.
  9. Issue a GET request to http://localhost:55778/api/employees/12345 from Fiddler. Include the request header Accept-Language: en-us. Web API returns the date as 6/2/1990 12:00:00 AM.
  10. Just as in the previous exercise, in order to get the formatting to work correctly with XML, we have to make changes to the Employee class. See Listing 4-40.

    Listing 4-40.  Using OnSerializing Callback for DateTime

    [DataContract]
    public class Employee
    {
        [DataMember]
        public int Id { get; set; }
     
        [DataMember]
        public string FirstName { get; set; }
     
        [DataMember]
        public string LastName { get; set; }
     
        public DateTime Doj { get; set; }
            
        public decimal Compensation { get; set; }
     
        [DataMember(Name = "Compensation")]
        private string CompensationSerialized { get; set; }
     
        [DataMember(Name = "Doj")]
        private string DojSerialized { get; set; }
     
        [OnSerializing]
        void OnSerializing(StreamingContext context)
        {
            this.CompensationSerialized = this.Compensation.ToString();
            this.DojSerialized = this.Doj.ToString();
        }
    }
     

Summary

Content negotiation is the process of selecting the best representation for a given response when there are multiple representations available. It is not called format negotiation, because the alternative representations may be of the same media type but use different capabilities of that type, they may be in different languages, and so on. The term negotiation is used because the client indicates its preferences. A client sends a list of options with a quality factor specified against each option, indicating the preference level. It is up to the service, which is Web API in our case, to fulfill the request in the way the client wants, respecting the client preferences. If Web API is not able to fulfill the request the way the client has requested, it can switch to a default or send a 406 - Not Acceptable status code in the response.

Character encoding denotes how the characters—letters, digits, and other symbols—are represented as bits and bytes for storage and communication. The HTTP request header Accept-Charset can be used by a client to indicate how the response message can be encoded. ASP.NET Web API supports UTF-8 and UTF-16 out of the box.

Content coding is the encoding transformation applied to an entity. It is primarily used to allow a response message to be compressed. An HTTP response message is compressed before it is sent from the server, and the clients indicate their preference for the compression schema to be used in the request header Accept-Encoding. A client that does not support compression can opt out of compression and receive an uncompressed response. The most common compression schemas are gzip and deflate. The .NET framework provides classes in the form of GZipStream and DeflateStream to compress and decompress streams.

The Accept-Language request header can be used by clients to indicate the set of preferred languages in the response. The same header can be used to specify locale preferences.

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

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