Overview
最近被序列化,循環引用的問題,讓我渾身酸爽。遇到這種異常是在搭建WebApi的時候,當我返回Linq實例類集合的時候出現的。
下定決心要解決這個問題。循環引用引起的原因是:
比如說:我現在有兩個 類 A 和 B 現在 A類中有B類類型的屬性存放着B類的對象,而B類中有一個屬性,存放置A類型的字段。結果,進行序列化的時候,序列化A的時候,因為要序列化這B類對象的屬性,然后去序列化B類型的對象,B類型的對象有一個屬性放置A類型的對象,然后又去序列化A ,如此循環往復。
說的可能有點抽象,下面我們還是來看代碼吧。
會引發異常的代碼
一個簡陋的Parent類
namespace 序列化循環引用
{
public class Parent
{
public string Name { get; set; }
public string Age { get; set; }
public string Gender { get; set; }
public Child Child { get; set; }
}
}
一個更簡陋的Child類
namespace 序列化循環引用
{
public class Child
{
public string Name { get; set; }
public Parent Parent { get; set; }
}
}
PS:這兩個簡陋的類,看起來平淡無奇,但是這里有兩個地方需要注意一下,Parent類中,有一個【Child】類型的屬性,Child類中有個【Parent】類型的數據。然后下面我要進行一個腦殘而又神奇的操作。
private void Form1_Load(object sender, EventArgs e)
{
Child child = new Child();
Parent person = new Parent() { Name = "魯迅認識的那只猹", Age = "18", Gender = "男", Child = child };
child.Name = "Test";
child.Parent = person;
JavaScriptSerializer jss = new JavaScriptSerializer();
string value = jss.Serialize(jss);
Console.WriteLine(value);
}
代碼看上去沒有問題,但是如果運行程序就會拋出異常

為什么要做這種腦殘的操作
我們自己設計的話,當然我想大家,一般情況下是不會這么做的。除非想找一下刺激。但是這種看似腦殘的操作,在ORM框架中被廣泛的應用,所以說這個操作有些時候並不腦殘。
建立一個測試用數據庫 表結構如下

填充了一些數據
User表

Department 表

查看Linq生成的實體類
Department
[global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.[User]")]
public partial class User : INotifyPropertyChanging, INotifyPropertyChanged
{
private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);
private int _Id;
private string _Username;
private string _Password;
private int _DepartmentId;
private EntityRef<Department> _Department;
/*************省略其他代碼****************/
}
User
[global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.Department")]
public partial class Department : INotifyPropertyChanging, INotifyPropertyChanged
{
private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);
private int _DepartmentId;
private string _Name;
private EntityRef<User> _User;
/*************省略其他代碼****************/
}
分析
我們看到 【User】表中引用了【Department】的對象,而【Department】中引用了【User】中的對象,如果我們這樣進行序列化的話,一定會出現很有意思的事情 。
為什么微軟要這么做呢,先來看看官方的解釋。
Provides for deferred loading and relationship maintenance for the singleton side of a one-to-many relationship in a LINQ to SQL application.
為在LINQ到SQL應用程序的一對多關系的單例方面提供延遲加載和關系維護。(感謝有道詞典...)
延遲加載我們在寫代碼的時候可能沒有什么感覺,但是關系維護 這一點大家應該深有體會。比如說下面的代碼:
TestDBDataContext db = new TestDBDataContext();
var result = from a in db.Users
where 1 == 1
select new
{
用戶名 = a.Username,
密碼 = a.Password,
部門名稱 = a.Department.Name
};
通過【User】實體類,可以快速的關聯到【Department】的屬性,從而拿到數據。配合我們VS強大的智能感知,可以進行爽快的開發。正是因為這點ORM框架才做出了看似有點問題的設計。
解決問題
上面的講解,是為了省事,建立了一個Winfrom的項目,到了解決問題的時候了我們需要建立一個【WebApi】的項目。
以下的配置將是我們解決問題的時候的默認配置。
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
/*************省略的不相干的代碼*******************/
//干掉Xml序列化
config.Formatters.Remove(config.Formatters.XmlFormatter);
}
}
獲取User表的數據
public class TestController : ApiController
{
public IEnumerable<object> GetAllUser()
{
Models.DBDataContext db = new Models.DBDataContext();
return db.Users;
}
}
在瀏覽器中進行瀏覽,會出現以下的錯誤頁面,其中會有一條類似下面的錯誤信息
Self referencing loop detected with type 'TestApi.Models.User'. Path '[0].Department.Users
referencing loop 是關鍵,告訴我們出現了循環引用.
因為ORM框架出於開發效率的設計,造成了我們序列化的時候,循環引用的問題。經過網上資料的查找找到了如下的解決方案。
- 使用匿名類,手動的避免循環引用的狀況
- 使用【[Newtonsoft.Json.JsonIgnore]】特性,標識引用對象,不進行序列化
- 更改WebApi的配置,忽略循環引用的序列化【推薦】
1 使用匿名類
public class TestController : ApiController
{
public IEnumerable<object> GetAllUser()
{
Models.DBDataContext db = new Models.DBDataContext();
//return db.Users;
var result = from a in db.Users
select new
{
a.Username,
a.Password,
a.Department.Name
};
return result;
}
}
/*
輸出結果
[{"Username":"魯迅認識的那只猹","Password":"123456","Name":"人事部"},{"Username":"被魯迅認識的那只猹","Password":"123456","Name":"人事部"},{"Username":"猹","Password":"123456","Name":"財務部"}]
*/
因為使用了匿名類,匿名類中也沒有出現,會出現循環引用的情況,所以這是一種解決方案。
2 使用[Newtonsoft.Json.JsonIgnore]特性標記
public IEnumerable<Models.User> GetAllUser()
{
Models.DBDataContext db = new Models.DBDataContext();
return db.Users;
}
直接像上面這么寫肯定是報錯了,我們需要對Linq生成的實體類稍微做一下修改
[global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.[User]")]
public partial class User : INotifyPropertyChanging, INotifyPropertyChanged
{
/*************省略的不相干的代碼*******************/
/*標記 Department 屬性在序列化的時候忽略掉*/
[Newtonsoft.Json.JsonIgnore]
[global::System.Data.Linq.Mapping.AssociationAttribute(Name="Department_User", Storage="_Department", ThisKey="Id", OtherKey="DepartmentId", IsForeignKey=true)]
public Department Department
{
get
{
return this._Department.Entity;
}
set
{
Department previousValue = this._Department.Entity;
if (((previousValue != value)
|| (this._Department.HasLoadedOrAssignedValue == false)))
{
this.SendPropertyChanging();
if ((previousValue != null))
{
this._Department.Entity = null;
previousValue.User = null;
}
this._Department.Entity = value;
if ((value != null))
{
value.User = this;
this._Id = value.DepartmentId;
}
else
{
this._Id = default(int);
}
this.SendPropertyChanged("Department");
}
}
}
/*************省略的不相干的代碼*******************/
}
再次運行,成功序列化
[{"Id":1,"Username":"魯迅認識的那只猹","Password":"123456","DepartmentId":1},{"Id":2,"Username":"被魯迅認識的那只猹","Password":"123456","DepartmentId":1},{"Id":3,"Username":"猹","Password":"123456","DepartmentId":2}]
3 直接進行配置為忽略循環引用 【推薦】
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
/*************省略的不相干的代碼*******************/
//干掉Xml序列化
config.Formatters.Remove(config.Formatters.XmlFormatter);
//設置JSON序列化遇到循環引用的處理方式
config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
}
}
運行程序,成功序列化
[{"Id":1,"Username":"魯迅認識的那只猹","Password":"123456","DepartmentId":1,"Department":{"DepartmentId":1,"Name":"人事部"}},{"Id":2,"Username":"被魯迅認識的那只猹","Password":"123456","DepartmentId":1,"Department":{"DepartmentId":2,"Name":"財務部"}},{"Id":3,"Username":"猹","Password":"123456","DepartmentId":2,"Department":null}]
結語
關於JSON序列化出現循環引用的問題的解決方案,一共有三種
使用匿名類使用特性標識直接進制WebApi配置,這幾種推薦結合的使用,並不是說一種方法可以解決所有的問題,還是要視情況而定,選擇最適合的。本文就到此為止,文中有任何紕漏之處,還望大家多多指出,以免誤導大家。
