文章目錄
- (1)缺乏不帶參數的建構函式
- (2)類型沒有被定義為public
本文的內容很多來自:http://www.asp.net/web-api/overview/formats-and-model-binding/json-and-xml-serialization
我狗尾續貂地添加些額外的說明與見解,或許對英文不太好的朋友有些用。
ASP.net的Web API和傳統MVC網站有個很大的不同就是多了Formatter(格式化器),其實Formatter並不是什麼新鮮東西,我覺得它只是另一種 ModelBinder 方法,簡單地說,就是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 usevoid 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; } }