這是今天作項目支持的發現的一個關于WCF的問題,雖然最終我只是添加了一行代碼就解決了這個問題,但是整個糾錯過程是痛苦的,甚至最終發現這個問題都具有偶然性。具體來說,這是一個關于如何自動為服務接口(契約)的每個操作添加FaultContract與WCF服務元數據發布的問題。接下來通過一個簡單的實例來說明這個因為少寫了一行代碼引發的血案。
一、手工添加FaultContract
WCF采用基于消息的通信方式,Endpoint的ABC三要素之一的契約(Contract)的本質就是定義消息的結構。契約不僅定義了正常請求和響應負載的結構,還定義了承載異常信息的響應消息的結構。為了讓契約能夠響應消息承載的錯誤信息,承載錯誤信息的類型需要利用FaultContractAttribute特性注冊到服務接口的操作方法上。
1: [ServiceContract]
2: public interface IMyService
3: {
4: [OperationContract]
5: [FaultContract(typeof(ServiceExceptionInfo))]
6: string GetData(int value);
7: }
8:
9: public class MyService : IMyService
10: {
11: public string GetData(int value)
12: {
13: var ex = new InvalidOperationException("Invalid operation...");
14: throw new FaultException<ServiceExceptionInfo>(new ServiceExceptionInfo(ex));
15: }
16: }
17:
18: [DataContract]
19: public class ServiceExceptionInfo
20: {
21: [DataMember]
22: public string ExceptionType { get; set; }
23:
24: [DataMember]
25: public string Message { get; set; }
26: public ServiceExceptionInfo(Exception ex)
27: {
28: this.ExceptionType = ex.GetType().AssemblyQualifiedName;
29: this.Message = ex.Message;
30: }
31: }
如下面的代碼片段所示,由于GetData操作拋出的FaultException對象采用一個ServiceExceptionInfo來描述詳細錯誤信息,所以我們在定義服務接口的時候需要利用FaultContractAttribute將ServiceExceptionInfo這個類型注冊到GetData方法上。
二、利用自定義ServiceHost自動注冊ServiceExceptionInfo類型
如果多個操作都需要注冊這么一個ServiceExceptionInfo類型,這其實是一件很繁瑣的事情。對于今天找我們作技術支持的那個項目來說,由于采用了我們提供的一個自動化異常處理框架,要求所有的操作都需要注冊一個類似于ServiceExceptionInfo的類型來描述最終的錯誤消息。為了讓具體的項目可以不用在每個操作上都添加一個FaultContractAttribute,我們自定義了一個ServiceHost來實現了對它的自動注冊。如下所示的MyServiceHost模擬了FaultContract自動化注冊的邏輯。
1: public class MyServiceHost: ServiceHost
2: {
3: public MyServiceHost(Type serviceType, params Uri[] baseAddresses) : base(serviceType, baseAddresses)
4: { }
5:
6: protected override void OnOpening()
7: {
8: base.OnOpening();
9: foreach (var endpoint in this.Description.Endpoints)
10: {
11: string ns = endpoint.Contract.Namespace.TrimEnd('/');
12: foreach (var op in endpoint.Contract.Operations)
13: {
14: if (!op.Faults.Any(it => it.DetailType == typeof(ServiceExceptionInfo)))
15: {
16: FaultDescription fault = new FaultDescription($"{ns}/{op.Name}_ServiceExceptionInfo");
17: fault.DetailType = typeof(ServiceExceptionInfo);
18: op.Faults.Add(fault);
19: }
20: }
21: }
22: }
23: }
24:
25: public class MyServiceHostFactory : ServiceHostFactory
26: {
27: protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
28: {
29: return new MyServiceHost(serviceType, baseAddresses);
30: }
31: }
MyServiceHostFactory是MyServiceHost對應的工廠,我們可以采用如下的配置使用它。
1: <system.serviceModel>
2: <behaviors>
3: <serviceBehaviors>
4: <behavior>
5: <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
6: <serviceDebug includeExceptionDetailInFaults="true"/>
7: </behavior>
8: </serviceBehaviors>
9: </behaviors>
10: <services>
11: <service name="WcfService.MyService">
12: <endpoint binding="basicHttpBinding" contract="WcfService.IMyService"/>
13: </service>
14: </services>
15: <serviceHostingEnvironment >
16: <serviceActivations>
17: <add service="WcfService.MyService" relativeAddress="myservice.svc" factory="WcfService.MyServiceHostFactory"/>
18: </serviceActivations>
19: </serviceHostingEnvironment>
20: </system.serviceModel>
三、獲取元數據(WSDL)受阻
在真的WCF服務調用過程中,我們定義的這個MyServiceHost和MyServiceHostFactory一點問題都沒有。但是一旦我們利用HTTP-GET獲取元數據(WSDL)的時候,會發生如下所示的NullReferenceException異常。
四、一行代碼解決這個問題
由于自定義的這個MyServiceHost的代碼實在太簡單,我實在想不到那個地方導致WsdlExporter的CreateWsdlOperationFault方法(根據Stacktrace,這個異常是從這個方法中拋出來的)。沒有辦法,只有看WCF的源代碼了,這個過程是很痛苦的,因為涉及的代碼太多,而且根本不知道這個Null Reference究竟是哪個變量。
既然查看源代碼并沒有真正解決這個問題,我們還得從自定義的這個MyServiceHost上找原因。這個MyServiceHost的作用簡單明了,就是為所有的操作添加一個針對ServiceExceptionInfo類型的FaultDescription對象而已,那么是不是因為添加的FaultDescription對象缺少了某些屬性導致的這個異常呢?為此,我將FaultDescription的所有屬性都進行了設置,最終發現只要按照如下的方式設置它的Name屬性就可以了。
1: public class MyServiceHost: ServiceHost
2: {
3: public MyServiceHost(Type serviceType, params Uri[] baseAddresses) : base(serviceType, baseAddresses)
4: { }
5:
6: protected override void OnOpening()
7: {
8: base.OnOpening();
9: foreach (var endpoint in this.Description.Endpoints)
10: {
11: string ns = endpoint.Contract.Namespace.TrimEnd('/');
12: foreach (var op in endpoint.Contract.Operations)
13: {
14: if (!op.Faults.Any(it => it.DetailType == typeof(ServiceExceptionInfo)))
15: {
16: FaultDescription fault = new FaultDescription($"{ns}/{op.Name}_ServiceExceptionInfo");
17: fault.Name = "ServiceExceptionInfoFault";
18: fault.DetailType = typeof(ServiceExceptionInfo);
19: op.Faults.Add(fault);
20: }
21: }
22: }
23: }
24: }
文章列表