当使用WCF + Entity Framework时要小心,否则,很容易掉入各种陷阱。这里介绍两个在序列化时容易遇到的、会导致服务停止的陷阱。
一、试图序列化Entity Proxy类而导致服务停止。
二、序列化时出现死循环导致服务停止;
无论掉入哪一个陷阱,在客户端都会看到这样的对话框(点击看大图):
呃~这个图才对:
大致意思是会说:服务不在线或者客户端配置有问题或者Proxy类有问题。而如果跟着Error Details里的第一行Google,会找到许多文章关于调整数据缓存大小、调整操作超时时间……如果按照常规则调试方法——按图索骥找问题,那么就可能越陷越深。
查找问题时,首先要透过假象,看到问题的本质——WCF服务因为某些原因而停止了。其次,我们要具体分析问题的特殊性,结合经验,来查找问题。
服务停止有一个典型的原因,那就是序列化出问题了。由于在WCF中,序列化不需要我们写代码实现,它在为我们提供便利的同时,也埋下了伏笔。而EF的使用,又进一步将事情变得错综复杂。凡事有弊有利,往往是使用了EF这样的特殊性,让我们查找问题时也更具方向性。
EF中,有什么特性会与WCF交互?没错,我们常常把EF的实体类用作DataContract。EF的实体类,无论是生成的还是通过Code First写的,运行时,为了支持EF的一些“高级”功能,例如,LazyLoad,Runtime会从你的Entity类自动继承出一个对应的代理类(Proxy Types)并对代理类对象进行操作。例如Person类,在运行时或者就会对应有一个叫Person123456类(肯定不会叫这个名字啦,这权当标识一下)。我们写代码时对Person的操作,运行时会换成对Person123456的操作。
通过调试窗口,我们可以到看到这个隐藏在背后的类(代码类为Handbook,实际类为DynamicProxies.Handbook_AE28…84):
这样一来,WCF,具体的说,是DataContractSerializer就不认账了——我只认识老子(Handbook),不认识儿子(代理类),没法序列化!于是它抛出了异常,导致服务中断。这样一来,我们会看到,用WCF返回一般的对象没有问题,一旦返回实体类对象,就会遇到了文章开始时看到的错误。
一个简单的解决方法:让DbContext使用静态类。在对应的DbContext的构造函数里添加一个设置即可:
1: public partial class MTBContainer : DbContext
2: {
3: public MTBContainer()
4: : base("name=MTBContainer")
5: {
6: this.Configuration.ProxyCreationEnabled = false; // This is the line added.
7: }
8: //...
9: public IDbSet<PlacePictureType> PlacePictureTypes { get; set; }
10: //...
11: }
第二个容易遇到的问题是序列化死循环的问题。乍一看错误跟第一个显示的是一样的:服务不可用了!而且同样是序列化问题。
但是,这类问题有一个特别的地方:有些实体类对象能正常返回,有些不能。
经过观察分析发现,不能返回的类往往是那些有Navigation属性的实体类,进一步分析,Navigation属性往往会构成对自身的间接引用:例如在一对多表里,往往一端有一个多端对象的集合,同时,多端又有一个一端的引用。比如有一个Parent实体,有一个Child实体,那么,Parent里会有一个ICollection<Child>用来存储所有的孩子,而Child里又会有一个Parent用来指明当前这个Child的Parent。这种互相引用的关系导致了DataContractSerializer在序列化它时出现了问题。
看来,不光光是在EF中会遇到这样的问题,DataContractSerializer序列化一个直接或者间接引用了自己的类对象,就会陷入死循环。
说话……Visual Studio的图标是不是像一个死循环?^v^
回正题,以上面描述的类为例:
1: //[DataContract(IsReference = true)]
2: [DataContract]
3: public class Parent
4: {
5: public Parent()
6: {
7: Children = new List<Child>();
8: }
9: [DataMember]
10: public string Name { get; set; }
11: [DataMember]
12: public IList<Child> Children { get; set; }
13: }
14:
15: //[DataContract(IsReference = true)]
16: [DataContract]
17: public class Child
18: {
19: [DataMember]
20: public string Name { get; set; }
21: [DataMember]
22: public Parent ParentRef { get; set; }
23: }
1: // Create mock objects
2: Parent p = new Parent() { Name = "Baba" };
3: Child c1 = new Child() { Name = "John", ParentRef = p };
4: Child c2 = new Child() { Name = "Alice", ParentRef = p };
5: p.Children.Add(c1);
6: p.Children.Add(c2);
7:
8: // Creat DataContractSerializer
9: DataContractSerializer serializer = new DataContractSerializer(typeof(Parent));
10:
11: // Serialize & output results.
12: string result = null;
13: using (Stream s = new MemoryStream())
14: {
15: serializer.WriteObject(s, p);
16: s.Seek(0, SeekOrigin.Begin);
17:
18: using (StreamReader r = new StreamReader(s))
19: {
20: result = r.ReadToEnd();
21: }
22: }
23: Console.WriteLine(result);
运行结果是:Crash。默认情况下,DataContractSerializer是这么把对象层层展开:
1: <Parent>
2: <Children>
3: <Child>
4: <Parent> <!—- this is Child.Parent Property… Enter into endless loop -->
5: <Children>
它不会顾及引用关系,只会一味的把对象的属性展成对应的XML元素。因此,Parent –> Child1 –> Parent –> Child1…无穷匮也了。
解决方法一、已经在上面的代码里了:在DataContract上使用IsReference参数,并且设置为true。([DataContract(IsReference = true)])。
解决方法二、调用DataContractSerializer的另一个构造函数来替代上面代码中的第9行:
1: DataContractSerializer serializer = new DataContractSerializer(typeof(Parent),
2: "Parent",
3: string.Empty,
4: null,
5: int.MaxValue,
6: false,
7: true,
8: null,
9: null);
第7行用来表明使用Preserve Object Reference。使用了Preserve Object Reference以后,会产生类似以下代码的序列化结果,每个元素都会有其对应的Id,对象引用对引用Id:
1: <Parent z:Id="i1" xmlns="http://schemas.datacontract.org/2004/07/Sealize" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
2: <Children>
3: <Child z:Id="i2">
4: <Name>John</Name>
5: <ParentRef z:Ref="i1"/>
6: </Child>
7: <Child z:Id="i3">
8: <Name>Alice</Name>
9: <ParentRef z:Ref="i1"/>
10: </Child>
11: </Children>
12: <Name>Baba</Name>
13: </Parent>
参考资源
Little knowledge is dangerous.