浅析ASP.Net Web API的Formatter

本文的内容很多来自: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;
        }
    }

 

posted @ 2012-10-12 14:41  guogangj  阅读(2894)  评论(0编辑  收藏  举报