Web API <五> 序列化
在 Asp.Net Web Api 中提供了两种 媒体类型格式化器(mime-type formatter),分别用于支持 JSON 和 XML 数据的格式化处理。默认两种格式化器已集成到了 Asp.Net Web Api 的请求处理管道(pipline) 中,客户端可以在请求报文头中通过设置 Accept 参数来指定获取数据的格式类型(JSON或 XML)。
媒体类型格式化器 是指具有如下功能的类型:
- 从 Http 消息中读取 CLR 对象
- 将 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; } // 序列化时被忽略
}
当然,还存在一种与上述情况相反的情况,即出去指定的属性或字段,忽略其它的。这种情况可以搭配使用 DataContract
和 DataMember
属性(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) 列表中移除 Json 或 Xml 格式化器,例如,基与下面的原因可能需要采取这种操作
- 默认情况下,客户端可以通过设置 **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>
*注意* 在实用上述的保留对象引用的功能时,要考虑接受数据的客户端是否能够解析这样格式的数据,在多数情况下应该尽量避免对象之间的循环引用。