問題描述
如果你在WCF中用Entity Framework來獲取數據并返回實體對象,那么對下面的錯誤一定不陌生。
接收對 http://localhost:5115/ReService.svc 的 HTTP 響應時發生錯誤。這可能是由于服務終結點綁定未使用 HTTP 協議造成的。
這還可能是由于服務器中止了 HTTP 請求上下文(可能由于服務關閉)所致。有關詳細信息,請參見服務器日志。
這就是因為在返回數據的時候,序列化失敗,導致WCF服務自動停止了。
為什么會序列化失敗
為了方便說明,我們先做個示例來重現這個錯誤。
默認情況下,Entity Framework為了支持它的一些高級特性(延遲加載等),默認將自動生成代理類是設置為true,即
public MyContext() { this.Configuration.ProxyCreationEnabled = true; }
這樣,如果我們的實體中包含其它實體的導航屬性,則EF會自動的為這個實體生成代理類。
[DataContract(IsReference=true)] public class Student { public Student() { this.Teachers = new HashSet<Teacher>(); } [DataMember] public int ID { get; set; } [DataMember] public virtual string Name { get; set; } [DataMember] public virtual ICollection<Teacher> Teachers { get; set; } } [DataContract(IsReference = true)] public class Teacher { [DataMember] public int ID { get; set; } [DataMember] public virtual string Name { get; set; } }
觀察上面兩個實體,Student中有對Teacher的導航屬性,而Teacher則沒有。我們看看通過EF對獲取這兩個對象有什么不同的情況
我們可以看到EF為Student生成了值為System.Data.Entity.DynamicProxies.Student_...的代理實體
而對于Teacher,返回的就是我們所定義的實體。
如果我們在WCF中分別定義一個契約,來返回這兩個實體會怎么樣呢?
[OperationContract]
Student GetStudent();
[OperationContract]
Teacher GetTeacher();
實現方法
public Student GetStudent() { using (MyContext context = new MyContext()) { return context.Students.FirstOrDefault(); } } public Teacher GetTeacher() { using (MyContext context = new MyContext()) { return context.Teachers.FirstOrDefault(); } }
調用 WCF進行測試,我們可以很好的得到GetTeacher()的值,如圖
但是,當調用GetStudent()方法,從服務端返回結果到客戶端時確報錯了。
嗯,沒錯,就是剛開始我說的那個錯誤。但,這是為什么呢。我們明明在Student中加了DataContract和DataMember關鍵字啊。
原因就是EF自動為Student生成了代理類,WCF序列化的其實是EF生成的那個代理類,而不是我們自己定義的Student,而代理類并沒有標識這是一個可以序列化的實體。
解決方法
1.禁用代理類
既然原因是EF生成了代理類,那我們把它禁用了就可以了嘛。也很簡單,只要將生成代理的配置設置為false即可。
public MyContext() { this.Configuration.ProxyCreationEnabled = false; }
禁用后,看看通過EF獲取Student是怎么樣的。
沒錯,代理類沒了,但是我們不能直接通過導航屬性來獲取Teacher了。這可是殺敵一千,自損八百啊。有沒有更好的辦法呢?
2 反序列化
既然代理類是由實體序列化而來的,我們就可以在返回數據前將代理類序列化成我們所需要的實體。
public Student GetStudent() { using (MyContext context = new MyContext()) { var stu=context.Students.FirstOrDefault(); var serializer = new DataContractSerializer(typeof(Student), new DataContractSerializerSettings() { DataContractResolver = new ProxyDataContractResolver() }); using (var stream = new MemoryStream()) { // 反序列化 serializer.WriteObject(stream, stu); stream.Seek(0, SeekOrigin.Begin); var newStu = (Student)serializer.ReadObject(stream); return newStu; } } }
通過這個方法,再測試一下.
不錯,沒有報錯,并且成功的得到了我們想要的結果。
但每個方法都要這樣序列化一下,是不是很麻煩,有沒有更好的方法。
答案肯定有,我們可以通過自定義Attribute,加在服務契約上面,標識通過這個服務返回的方法都要進行反序列化。
public class ProxyDataContractResolver: DataContractResolver { private XsdDataContractExporter _exporter = new XsdDataContractExporter(); public override Type ResolveName( string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver) { return knownTypeResolver.ResolveName( typeName, typeNamespace, declaredType, null); } public override bool TryResolveType(Type dataContractType,Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace) { Type nonProxyType = ObjectContext.GetObjectType(dataContractType); if (nonProxyType != dataContractType) { // Type was a proxy type, so map the name to the non-proxy name XmlQualifiedName qualifiedName = _exporter.GetSchemaTypeName(nonProxyType); XmlDictionary dictionary = new XmlDictionary(2); typeName = new XmlDictionaryString(dictionary, qualifiedName.Name, 0); typeNamespace = new XmlDictionaryString(dictionary, qualifiedName.Namespace, 1); return true; } else { // Type was not a proxy type, so do the default return knownTypeResolver.TryResolveType( dataContractType, declaredType, null, out typeName, out typeNamespace); } } }
public class ApplyProxyDataContractResolverAttribute : Attribute, IOperationBehavior { public void AddBindingParameters(OperationDescription description, BindingParameterCollection parameters) { } public void ApplyClientBehavior(OperationDescription description, ClientOperation proxy) { DataContractSerializerOperationBehavior dataContractSerializerOperationBehavior = description.Behaviors.Find<DataContractSerializerOperationBehavior>(); dataContractSerializerOperationBehavior.DataContractResolver = new ProxyDataContractResolver(); } public void ApplyDispatchBehavior(OperationDescription description, DispatchOperation dispatch) { DataContractSerializerOperationBehavior dataContractSerializerOperationBehavior = description.Behaviors.Find<DataContractSerializerOperationBehavior>(); dataContractSerializerOperationBehavior.DataContractResolver = new ProxyDataContractResolver(); } public void Validate(OperationDescription description) { } }
類ApplyProxyDataContractResolverAttribute就是我們想要的結果。現在我們只要在定義服務契約的時候,加上ApplyProxyDataContractResolver關鍵字就可以了。
[OperationContract]
[ApplyProxyDataContractResolver]
Student GetStudent();
[OperationContract]
[ApplyProxyDataContractResolver]
Teacher GetTeacher();
擴展
對于繼承類的序列化,要在基類用KnownType屬性來標識
[KnownType(typeof(ClassB))] [KnownType(typeof(ClassA))] [DataContract] public class BaseClass { } [DataContract] public class ClassA : BaseClass { } [DataContract] public class ClassB : BaseClass { }
PS:雖然這樣可以解決問題,但是多一層序列化會影響效率,希望EF的后續版本可以解決問題吧。
文章列表