這是今天作專案支援的發現的一個關於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: }