WebApi-JSON序列化循環引用


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】中的對象,如果我們這樣進行序列化的話,一定會出現很有意思的事情 ![img](file:///C:\Users\IT\AppData\Local\Temp\SGPicFaceTpBq\20664\28CEF69B.png)。

為什么微軟要這么做呢,先來看看官方的解釋。

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框架出於開發效率的設計,造成了我們序列化的時候,循環引用的問題。經過網上資料的查找找到了如下的解決方案。

  1. 使用匿名類,手動的避免循環引用的狀況
  2. 使用【[Newtonsoft.Json.JsonIgnore]】特性,標識引用對象,不進行序列化
  3. 更改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配置 ,這幾種推薦結合的使用,並不是說一種方法可以解決所有的問題,還是要視情況而定,選擇最適合的。

本文就到此為止,文中有任何紕漏之處,還望大家多多指出,以免誤導大家。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM