Web API <五> 序列化

Asp.Net Web Api 中提供了两种 媒体类型格式化器(mime-type formatter),分别用于支持 JSONXML 数据的格式化处理。默认两种格式化器已集成到了 Asp.Net Web Api 的请求处理管道(pipline) 中,客户端可以在请求报文头中通过设置 Accept 参数来指定获取数据的格式类型(JSON或 XML)。

媒体类型格式化器 是指具有如下功能的类型:

  1. 从 Http 消息中读取 CLR 对象
  2. CLR 对象写入到Http 消息中

JSon 格式化器

  Json 格式化是通过 JsonMediaTypeFormatterl类实现的 ,默认情况下使用的是第三方开源库 JSon.Net 进行序列化操作的。如果愿意,还可以使用 Net 内置的 **DataContractJsonSerializer ** 来替代默认的 Json.Net ,只要在 Global.asax进行如下的配置即可。

	   var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
       json.UseDataContractJsonSerializer = true;

Json 序列化

  默认情况下,所有的公有的 属性(property)字段(field) 都会被序列化,包括那些只读的属性或字段,而忽略那些标记有 JsonIgnore 属性(Attribute)的属性和字段。

	public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    [JsonIgnore]
    public int ProductCode { get; set; } // 序列化时被忽略
}

  当然,还存在一种与上述情况相反的情况,即出去指定的属性或字段,忽略其它的。这种情况可以搭配使用 DataContractDataMember 属性(Attribute) 来实现。对于标记了 DataMember 的属性或字段,即使它是私有的,仍然会被序列化。

[DataContract]
public class Product
{
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public decimal Price { get; set; }
    public int ProductCode { get; set; }  // 默认被忽略
    [DataMember]
    private string PrivateField{ get; set; }//即使是私有属性也会被序列化
}
日期序列化

  默认情况下,Json.Net 会将 UTC 时间序列化为带有一个 Z 后缀的字符串,本地时间会被处理为一个包含时区偏移值(time-zone offset)的字符串,如下所示
  

  • 2017-08-10T16:24:03.5739377+08:00 //本地时间
  • 2017-08-10T08:24:42.8873723Z //UTC 时间

默认情况下,Json.Net 会在序列化时间(本地时间)会保留一个时区的偏移量,我们可以通过设置 **DateTimeZoneHandling ** 来进行更改。该属性是一个 DateTimeZoneHandling 类型的枚举,如下所示

	 //
    public enum DateTimeZoneHandling
    {
        //
        // 摘要:
        //    作为本地时间处理,如果是一个 UTC 时间会被转换为本地时间
        //     Time (UTC), it is converted to the local time.
        Local = 0,
        //
        // 摘要:
        //    作为UTC 时间来处理,如果是一个本地时间会被转换为 UTC 时间
        //     converted to a UTC.
        Utc = 1,
        //
        // 摘要:
        // 在序列化时总是将 System.Date 对象当作本地时间来处理,在反序列化时,如果指定了特定的时区,则将其转换为本地时间
        //     a string is being converted to System.DateTime, convert to a local time if a
        //     time zone is specified.
        Unspecified = 2,
        //
        // 摘要
        //当转换时总是保留时区信息,默认值
        RoundtripKind = 3

  除了使用默认的 ISO 8601 的格式来显示时间,还可以选择使用 Miscrosoft JSon Date Format (Date(tickets)) ,这个可以通过设置 DateFormatHandling 来实现,如下所示:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.DateFormatHandling = Newtonsoft.Json.DateFormatHandling.MicrosoftDateFormat;

  如果设置了 DateTimeZoneHandling 属性,那么在这里显示的时候是不同的,本地时间会有一个时区偏移值,

   Date(1502433077121+0800)//本地时间
   Date(1502433077121)//UTC时间

Json 的格式缩进

  默认情况下返回的 JSon 数据时没有格式缩紧的,如果需要产生具有缩进格式的Json,可以通过设置 **CamelCasePropertyNamesContractResolver **,如下所示:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;

没有进行格式缩进的 JSon

	[{"id":1,"myname":"Computer","marketDate":"\/Date(1502433578649+0800)\/","type"  :0},{"id":2,"myname":"Telphone","marketDate":"\/Date(1502433578649)\/","type":1}]

具有格式缩进的 JSon

[
  {
    "id": 1,
    "myname": "Computer",
    "marketDate": "\/Date(1502433708247+0800)\/",
    "type": 0
  },
  {
    "id": 2,
    "myname": "Telphone",
    "marketDate": "\/Date(1502433708247)\/",
    "type": 1
  }
]

变量的驼峰式(Camel Casing)写法

  这里的驼峰式写法指的是 小驼峰写法,例如变量 MyName 会被转换为 myName
如果要启用变量的 驼峰式写法,可以通过如下的设置来实现:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

关于更多的驼峰式写法的信息看这里

返回匿名类型

  在 Action 方法中,可以直接返回一个匿名的 Object 对象,如下所示

public object Get()
{
    return new { 
        Name = "Alice", 
        Age = 23, 
        Pets = new List<string> { "Fido", "Polly", "Spot" } 
    };
}

  该对象会自动被序列化为一个 Json 字符串:{"Name":"Alice","Age":23,"Pets":["Fido","Polly","Spot"]}
  当接受一些客户端传的结构松散 [1]Json 对象数据时,可以使用 Newtonsoft.Json.Linq.JObject 来接受,如下所示:

public void Post(JObject person)
{
    string name = person["Name"].ToString();
    int age = person["Age"].ToObject<int>();
}

[1] 这里所说的结构松散是指json 对象在服务器端没有对应的 model 对象来供其进行反序列化

  但接受客户端的 json 对象的最好方法还是使用 Model 实体对象,因为这样不仅可以将 Json 对象与 Model 实体对象自动绑定,还可以自动进行必要的 Model 验证。

XML 格式化器

  XML 的格式化是通过 **XmlMediaTypeFormatter ** 类来实现的,默认是 **DataContractSerializer ** 执行 XML序列化。如果愿意,还可以配置 XmlMediaTypeFormat 使用 XmlSerializer 去替代默认的 DataContractSerializer

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
xml.UseXmlSerializer = true;

  XmlSerializer 仅支持 DataContractSerializer 全部功能的一部分,但却可以对结果的 Xml进行更多的控制。 当你需要匹配现有 xml 模式(Schema)

Xml 序列化

  使用默认的 DataContractSerializer 时,会按照如下的规则进行序列化:

  • 所有的字段或 Get/Set 的属性都将被序列化,忽略那些标记有 **IgnoreDataMember ** 属性的属性或字段
  • 所有私有(Private) 和 受保护(Protected) 的成员不会被序列化
  • 只读属性不会被序列化,但只读集合成员的集合元素会被序列化
  • 被序列化的类型的类型名称和成员名称将出现在序列化产生中的 Xml
  • 会有一个默认的 Xml 命名空间

  下面通过一个例子对上述的规则进行进一步的说明:定义的 Model 实体 Product 如下所示

	/// <summary>
    /// 商品类
    /// </summary>
    public class Product
    {
        private string[] _Tags;
        public Product()
        {
            _Tags = new string[]
            {
                "Tag1","Tag2"
            };
        }
        /// <summary>
        /// 商品的Id
        /// </summary>
        public int Id { get; set; }
        /// <summary>
        /// 商品的名称
        /// </summary>
        public string myname { get; set; }

        /// <summary>
        /// 上市时间
        /// </summary>

        public DateTime MarketDate
        {
            get; set;
        }
        //只读集合属性
        public string[] Tags
        {
            get
            {
                return this._Tags;
            }
        }
        private string PrivateField { get; set; }

        public ProductType Type { get; set; }
    }

    public enum ProductType
    {
        Type1,
        Type2
    }

  调用 Web Api 方法返回 Xml 序列化后的 Product 数组,返回的 Xml 如下所示:

	<ArrayOfProduct xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/WebApi.Models">
    <Product>
        <Id>1</Id>
        <MarketDate>2017-08-11T16:54:39.4142813+08:00</MarketDate>
        <Tags xmlns:d3p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
            <d3p1:string>Tag1</d3p1:string>
            <d3p1:string>Tag2</d3p1:string>
        </Tags>
        <Type>Type1</Type>
        <myname>Computer</myname>
    </Product>
    <Product>
        <Id>2</Id>
        <MarketDate>2017-08-11T08:54:39.4142813Z</MarketDate>
        <Tags xmlns:d3p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
            <d3p1:string>Tag1</d3p1:string>
            <d3p1:string>Tag2</d3p1:string>
        </Tags>
        <Type>Type2</Type>
        <myname>Telphone</myname>
    </Product>
</ArrayOfProduct>

  如果需要对序列化的结果又更多的控制,那么可以对将要序列化的类型使用 DataContract 进行标记,被标记的类型将按照如下的规则进行序列化:

  • 属性和字段默认不被序列化,只有标记为 DataMember 的成员才会被序列化
  • 私有(Private) 和 保护(Protected) 的成员如果被标记为 DataMember 也将会被序列化
  • 只读属性不会被序列化
  • 设置 DataContract 属性的 Name 的参数可以自定义序列化结果 Xml 中的类型名称
  • 设置 DataMember 属性的 Name 参数可以自定义序列化 后结果 Xml 中对应字段或属性的名称
  • 设置 DataContract 属性的 NameSpace 参数可以自定义序列化后结果中的命名空间的名称
只读属性

  只读属性不会被序列化,但如果该属性后面有一个私有的字段的话,那么可以在该私有字段上加以 DataMember 修饰 ,
  那么该私有字段便可以被序列化。

[DataContract]
public class Product
{
    [DataMember]
    private int pcode;  // serialized

    // Not serialized (read-only)
    public int ProductCode { get { return pcode; } }
}
Date 序列化

  Xml序列化中 ** Date** 全部使用 ISO 8601 格式,例如 2012-05-23T20:21:37.9116538Z

Xml 格式化缩进

  为了产生具有缩进格式的 Xml ,可以 将Intent 属性设置为 TRUE

	var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
    xml.Indent = true;
为每个 类型设置不同的序列化器

  可以为不同的 CLR 类型设置不同的序列化器,例如,在某种情况下,某个特定的数据对象为了保持良好的向后兼容 行,需要使用 XmlSerializer,这时便可为该类型使用 XmlSerializer,而为其它类型使用 DataConstractSerializer.
  可以调用 SetSerializer 方法来为某个类型设置特定的序列化器

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
// Use XmlSerializer for instances of type "Product":
xml.SetSerializer<Product>(new XmlSerializer(typeof(Product)));

移除 Json 或 Xml 格式化器

  我们可以从格式化器(formatter) 列表中移除 JsonXml 格式化器,例如,基与下面的原因可能需要采取这种操作

  • 默认情况下,客户端可以通过设置 **Http ** 请求头部的 Accept 字段来设置其接受的数据的媒体类型,如果要限制 Web API 仅返回指定的媒体类型(media type),例如 Json,那么便可以移除 XmlSerializer
  • 使用自定义的格式化器取代默认的格式化器。

下面的代码展示了如何移除默认的格式化器,需要在 App_Start 方法中调用

void ConfigureApi(HttpConfiguration config)
{
    // Remove the JSON formatter
    config.Formatters.Remove(config.Formatters.JsonFormatter);
    // or
    // Remove the XML formatter
    config.Formatters.Remove(config.Formatters.XmlFormatter);
}

处理对象的循环引用

  默认情况下,Json 序列化器和 Xml 序列化器会将所有的对象都序列化为字面值,如果两个属性引用了相同的对象,或者在一个集合中一个对象出现了两次,那么这个对象便会被序列化两次。如果一个对象的类型结构中存在循环引用时便会发生问题,因为序列化器在检测到对象结构中存在循环引用时便会抛出异常。

	public class Employee
{
    public string Name { get; set; }
    public Department Department { get; set; }
}

public class Department
{
    public string Name { get; set; }
    public Employee Manager { get; set; }
}

public class DepartmentsController : ApiController
{
    public Department Get(int id)
    {
        Department sales = new Department() { Name = "Sales" };
        Employee alice = new Employee() { Name = "Alice", Department = sales };
        sales.Manager = alice;
        return sales;
    }
}

  调用上面的 Action 方法 Get 会导致序列化器抛出一个异常,然后返回一个 500
的状态码响应给客户端。
  为了在生成的 JSon 数据中保留对象的引用,可以在 Global.asax 文件的 App_Start 方法中添加如下的代码:

	var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
    json.SerializerSettings.PreserveReferencesHandling = 
    Newtonsoft.Json.PreserveReferencesHandling.All;

  此时,该Action 返回的 Json 数据如下所示:

{"$id":"1","Name":"Sales","Manager":{"$id":"2","Name":"Alice","Department":{"$ref":"1"}}}

  序列化器给两个相互引用的对象都添加了一个 $id 属性,检测到 Employee.Department 创造了一个循环引用,一次使用 $ref 替代了引用的值。
  为了在 Xml 序列化过程中保留这用对象的引用关系,有两种可行的方法,第一种将 DataContract 属性的 IsReference 属性设置为 True,如下所示:

[DataContract(IsReference=true)]
public class Department
{
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public Employee Manager { get; set; }
}

  另一种方式是创建一个 **DataContractSerializer ** 类型的实例,然后在其构造函数中将其 preserveObjectReferences 属性设置为 True,然后将需要保留引用的类型的序列化器设置新创建的实例,如下所示:

	var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
    var dcs = new DataContractSerializer(typeof(Department), null, int.MaxValue, 
    false, /* preserveObjectReferences: */ true, null);
    xml.SetSerializer<Department>(dcs);

  生成的 Xml 如下所示:

<Department xmlns:i="http://www.w3.org/2001/XMLSchema-instance" z:Id="i1" 
            xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" 
            xmlns="http://schemas.datacontract.org/2004/07/Models">
  <Manager>
    <Department z:Ref="i1" />
    <Name>Alice</Name>
  </Manager>
  <Name>Sales</Name>
</Department>

 *注意* 在实用上述的保留对象引用的功能时,要考虑接受数据的客户端是否能够解析这样格式的数据,在多数情况下应该尽量避免对象之间的循环引用。

posted @ 2017-10-16 15:04  空城守望城空  阅读(341)  评论(0编辑  收藏  举报