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】中的对象,如果我们这样进行序列化的话,一定会出现很有意思的事情 。
为什么微软要这么做呢,先来看看官方的解释。
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配置,这几种推荐结合的使用,并不是说一种方法可以解决所有的问题,还是要视情况而定,选择最适合的。本文就到此为止,文中有任何纰漏之处,还望大家多多指出,以免误导大家。

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号