本文的内容很多来自:http://www.asp.net/web-api/overview/formats-and-model-binding/json-and-xml-serialization
我狗尾续貂地添加些额外的说明与见解,或许对英文不太好的朋友有些用。
ASP.net的Web API和传统MVC网站有个很大的不同就是多了Formatter(格式化器),其实Formatter并不是什么新鲜东西,我觉得它只是另一种 Model绑定方法,简单地说,就是HTTP的数据到.Net对象的关系。MVC的Model Binding做过MVC网站的人应该都很熟悉了,就是尝试从HTTP请求中找到一些“key=value”的键值对,根据一些约定,匹配到Model的属性或Action参数上去,如果Model中有个值类型(例如int)的属性,而HTTP请求中又没有,那么会出现ArgumentException异常,并默认显示出大家非常熟悉的YSOD(Yellow Screen Of Death)。
我想微软弄格式化器的原因是想让数据绑定具有更强的可伸缩性,想像一下:你可以用格式化器自定义日期的输出格式;通过格式化器,将一个字符串转变为一张小png图片输出;还有更清晰和明确地定义数据等等。
一、有哪些Formatter?
创建一个默认的MVC4 Web API工程,在WebApiConfig中加入:
foreach (var fmt in config.Formatters) { System.Diagnostics.Debug.WriteLine(fmt.GetType()); }
默认情况下,能在Output窗口下看到:
System.Net.Http.Formatting.JsonMediaTypeFormatter
System.Net.Http.Formatting.XmlMediaTypeFormatter
System.Net.Http.Formatting.FormUrlEncodedMediaTypeFormatter
System.Web.Http.ModelBinding.JQueryMvcFormUrlEncodedFormatter
能看到这些格式化器,一眼就看出来,JsonMediaTypeFormatter是用来负责JSON的序列化/反序列化的,XmlMediaTypeFormatter是用来负责XML的序列化/反序列化的,FormUrlEncodedMediaTypeFormatter用来处理URL带的请求参数,JQueryMvcFormUrlEncodedFormatter的处理内容应该跟表单数据相关。
二、Web API是怎么处理XML的?
一开始我以为Web API是用System.Xml.XmlSerializer来处理XML,但后来发现不是(默认不是)。很简单的证据就是:
XmlSerializer默认不能序列化IEnumerable,它会报错说IEnumerable是接口,如果你要序列化,恐怕得自行实现IXmlSerializable接口;但默认情况下,Web API能轻松地将IEnumerable<Order>这样的类型序列化为XML并返回给客户端。那Web API默认用了什么XML序列化器呢?——DataContractSerializer。
创建一个最简单的控制台程序,然后用下面的代码测试一下:
IEnumerable<string> testobj = new string[] { "aaa", "bbb", "ccc" }; DataContractSerializer ser = new DataContractSerializer(testobj.GetType()); ser.WriteObject(Console.OpenStandardOutput(), testobj); //Error //XmlSerializer ser = new XmlSerializer(test.GetType()); //ser.Serialize(Console.Out, test);
我个人觉得使用DataContractSerializer更好,也就是Web API默认的设置,很明显,能序列化IEnumerable对我们来说太必要了。但有些习惯了使用XmlSerializer相关序列化特性(如[XmlAttribute],[XmlRoot],[XmlElement]之类)的人会比较喜欢XmlSerializer,要这样做也很简单,只需要在WebApiConfig.cs中加入:
config.Formatters.XmlFormatter.UseXmlSerializer = true;
DataContractSerializer的序列化特性其实也很丰富,WCF的相关文章会有很详尽的描述,这里就不展开了。
三、用XML还是JSON?
Web API会自动选择,选择的依据是请求的报文的HTTP Header:
Accept: application/json
Content-type: application/json
Content-type表示请求的body中的数据类型,是JSON还是XML,还是图片或者别的;Accept表示这个请求所期待得到的数据类型。上面的请求表示请求报文body中的数据为JSON,所期待的返回的数据类型也是JSON。如果要XML,那么很简单,把application/json改为application/xml即可。
我通过实验发现,Web API还有一条规则:如果请求的的数据类型由于某些原因无法正常得到,那么尝试以别的数据类型来返回。比如请求XML,而XML在序列化的时候出现了问题,那么Web API会尝试用JSON返回数据。
四、序列化为XML失败的可能原因
XML和JSON,我更喜欢JSON,因为简洁,XML包含了太多的标签、Schema以及namespace,很容易让人眼花缭乱,但有些客户端处理XML更为便利,所以我们还是要考虑一下XML的序列化问题。
(1)缺乏不带参数的构造函数
如果你的class缺乏不带参数的构造函数,那么序列化成XML的时候就会报错,我一开始也想不明白为什么会这样,要个不带参数构造函数干什么呢?直接把里面该序列化的东西序列化好不就OK了吗?大家花一分钟时间想想看啊!反正我想不出来为什么,你要是能想出来的话说明你比我聪明,呵呵……OK,言归正传了,原因其实很简单:反序列化!
(2)类型没有被定义为public
这个限制的原因可能是:序列化器认为,对非public的类型序列化会破坏数据的封装性。
五、JSON的序列化器以及控制时间日期的格式
MVC4的正式版跟之前的Beta版有不少差异,其中一个就是把JSON的序列化器默认为Newtonsoft.Json,看来微软现在想得很开,都开始往自己的开发环境里加入第三方的库了。但需要知道Newtonsoft.Json则个库也是在不断更新的,最好用NuGet来获取其最新版本。
我个人认为Newtonsoft.Json是相当不错的,对IEnumerable,IDictionary等接口都支持得很好,下面是个简单的控制台例子,我们可以用它来观察JSON的序列化情况:
Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(testObject));
只有一行,是否非常简单?具体Newtonsoft.Json的使用可以参考它的官方站点:http://json.codeplex.com
我这里特别要说明的一个问题是关于JSON的日期格式问题,众所周知,使用JSON的客户端大多是浏览器,浏览器的Javascript对日期格式的处理能力是要远远差于C#/Java之类的,默认情况下,日期会被序列化为“2012-10-12T13:18:20.1656358+08:00”这样的格式,直接把这个显示出来明显不够友好,假如我光是想显示“2012-10-12”,是不是就得用Javascript去操作这个字符串截取前10个字符?这样很不优雅并且不能确保一定可行,如果默认格式不是这样呢?对吧。后来我研究出一种方法,能很好解决这个问题(花了不少时间来搜索,咳咳……),我们来给Newtonsoft.Json添些料:
namespace Newtonsoft.Json.Converters { public class SimpleDateConverter : DateTimeConverterBase { public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { DateTime date = new DateTime(); DateTime.TryParse((string)reader.Value, out date); return date; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { writer.WriteValue(((DateTime)value).ToString("yyyy-MM-dd")); } } }
然后像这样修饰DateTime类型的属性:
[JsonConverter(typeof(SimpleDateConverter))] public DateTime Dt {get;set;}
六、测试序列化和反序列化的一致性
序列化和反序列化必须保持一致,否则程序就会乱套,如何确保?当然是测试一下,本文的开头提供的那个链接指向的那篇文章,在文章最后就提供了一套很不错的方法,使用起来很简单,我这里就借用下它上面的代码:
string Serialize<T>(MediaTypeFormatter formatter, T value) { // Create a dummy HTTP Content. Stream stream = new MemoryStream(); var content = new StreamContent(stream); /// Serialize the object. formatter.WriteToStreamAsync(typeof(T), value, stream, content.Headers, null).Wait(); // Read the serialized string. stream.Position = 0; return content.ReadAsStringAsync().Result; } T Deserialize<T>(MediaTypeFormatter formatter, string str) where T : class { // Write the serialized string to a memory stream. Stream stream = new MemoryStream(); StreamWriter writer = new StreamWriter(stream); writer.Write(str); writer.Flush(); stream.Position = 0; // Deserialize to an object of type T return formatter.ReadFromStreamAsync(typeof(T), stream, null, null).Result as T; } // Example of use void TestSerialization() { var value = new Person() { Name = "Alice", Age = 23 }; var xml = new XmlMediaTypeFormatter(); string str = Serialize(xml, value); var json = new JsonMediaTypeFormatter(); str = Serialize(json, value); // Round trip Person person2 = Deserialize<Person>(json, str); }
这段代码写得很不错!
七,只返回XML或只返回JSON
如果你有特殊的需要,只允许返回XML或只允许返回JSON的话,(虽然很不建议这样)那么可以把对应的formatter拿掉即可。例如你可以这样拿掉XML Formatter(代码写在WebApiConfig.cs中):
foreach (var fmt in config.Formatters) { System.Diagnostics.Debug.WriteLine(fmt.GetType()); if (fmt is System.Net.Http.Formatting.XmlMediaTypeFormatter) { config.Formatters.Remove(fmt); break; } }