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配置 ,这几种推荐结合的使用,并不是说一种方法可以解决所有的问题,还是要视情况而定,选择最适合的。

本文就到此为止,文中有任何纰漏之处,还望大家多多指出,以免误导大家。

posted @ 2017-08-18 11:35  鲁迅认识的那只猹  阅读(2419)  评论(0编辑  收藏  举报