Customizing Response
Request for Comments (RFC) 2616 defines content negotiation as “the 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:
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.
Listing 4-1. The Employee Class
public class Employee
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
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);
}
}
}
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 = ""
},
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);
}
}
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":""}
Listing 4-5. Web API Request Asking for UTF-16
Host: localhost:55778
Accept-charset: utf-16
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":""}
Listing 4-7. Web API Request Asking for DBCS Character Encoding of Shift JIS
Host: localhost:55778
Accept-charset: shift_jis
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":""}
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!
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);
}
}
}
Host: localhost:55778
Accept-charset: shift_jis
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":""}
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.
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
}
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);
}
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));
}
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
}
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;
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();
}
public class NegotiationFailedException : ApplicationException { }.
It does not carry any additional information and just derives from ApplicationException.
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);
}
}
}
}
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;
}
}
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
}
}
Figure 4-1. Fiddler Responding to DEFLATE Encoding
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);
}
}
}
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;
}
}
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.
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);
}
}
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());
}
}
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;
}
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"}
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.
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);
}
}
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; }
}
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
}
{"Id":12345,"FirstName":"John","LastName":"Human","Compensation": 45678.12}
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());
}
}
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());
}
}
{"Id":12345,"FirstName":"John","LastName":"Human","Compensation":" 45678,12"}.
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();
}
}
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>
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.
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.
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; }
}
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
}
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());
}
}
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());
}
}
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.