WCF, being a service, needs to manage all its exceptions internally and let only the very basic and general information flow out. This although being a general design requirement for all the components, with WCF it becomes all the more necessary as the service may be running in its own Application Domain, and along with on another machine on the network. Since direct debugging would not be available in this case, any exceptions raised need to be properly handled, otherwise the application can suffer a big downtime.
WCF has exceptions represented as FaultContracts, which are data contracts specifically meant for exceptions (kind of logical "Decorator" pattern). To make a class used to denote exception, we need to inherit it with
FaultException
and when applying to an operation contract, mark the method with
FaultContract
attribute. This tells that the particular operation may have such listed faults as expected behavior. In case of any unlisted faults, the channel gets broken
and the whole system wireup is done again.
1: [DataContract(Namespace = "http://nitinsingh.com/code/wcf/")]
2: public class MyCustomFault : FaultException
3: {
4: public string FaultDetails { get; set; }
5: }
6:
Now the question is, can we always place such fault contracts on each necessary operation contract? Should be, but in real scenarios, its not possible always.
There would be always some cases that certain exceptions get missed.
The solution....
Create a 'catch-all' exception handler for uncaught ones. WCF offers service behaviors to customize how a given service acts in specific cases.
Within the behavior, we can get the dispatcher being used for the binding, and apply custom handling to any type of runtime event.
The IErrorHandler
functionality provides such an generic error handling. Any unhandled exception within the service interaction is caught by this through
its two methods - HandleError
and ProvideFault
and hence saves the channel.
To create this handler, we create a class inheriting from ServiceModel.Dispatcher.IErrorHandler
(for error handling) and also
ServiceModel.Description.IServiceBehavior
(to attach this handler to the service behavior). Following is the code:
1: using System;
2: using System.Configuration;
3: using System.Linq;
4: using System.ServiceModel;
5: using System.ServiceModel.Channels;
6: using System.ServiceModel.Description;
7: using System.ServiceModel.Dispatcher;
8: using WCFSamples.Contracts;
9: using WCFSamples.Service;
10:
11:
12: public class MyErrorHandler : IErrorHandler, IServiceBehavior
13: {
14: #region IErrorHandler Members
15:
16: public bool HandleError(System.Exception error)
17: {
18: if (error != null)
19: {
20:
21:
22:
23: return true;
24: }
25: return false;
26: }
27:
28: public void ProvideFault(System.Exception error,
29: System.ServiceModel.Channels.MessageVersion version,
30: ref System.ServiceModel.Channels.Message fault)
31: {
32:
33: if (!(error is FaultException))
34: {
35:
36: MyCustomFault customFault = new MyCustomFault() { FaultDetails = "Something gone wrong" };
37:
38:
39: FaultException>MyCustomFault< f = new FaultException>MyCustomFault<(
40: customFault, error.Message,
41: FaultCode.CreateSenderFaultCode("MyCustomFault", "http://nitinsingh.com/code/wcf/"));
42:
43: MessageFault mf = customFault.CreateMessageFault();
44:
45: fault = Message.CreateMessage(version, mf, Constants.Action);
46: }
47: }
48:
49: #endregion
50:
51: #region IServiceBehavior Members
52:
53: public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase,
54: System.Collections.ObjectModel.Collection endpoints,
55: System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
56: {
57: return;
58: }
59:
60: 61: 62: 63: public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
64: {
65: foreach (ChannelDispatcher dispatcher in serviceHostBase.ChannelDispatchers)
66: {
67: dispatcher.ErrorHandlers.Add(this);
68: }
69: }
70:
71: 72: 73: 74: public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
75: {
76: foreach (var svcEndPoint in serviceDescription.Endpoints)
77: {
78:
79: if (svcEndPoint.Contract.Name != "IMetaDataExchange")
80: {
81: foreach (var opDesc in svcEndPoint.Contract.Operations)
82: {
83:
84: if (opDesc.Faults.Count == 0)
85: {
86: string msg = string.Format(
"MyCustomEventHandlerBehvior requires a FaultContract(typeof(MyCustomFault))" +
87: " on each operation contract. The {0} contains no FaultContracts.",
88: opDesc.Name);
89: throw new InvalidOperationException(msg);
90: }
91:
92: var fcExists = from fc in opDesc.Faults
93: where fc.DetailType == typeof(MyCustomFault)
94: select fc;
95:
96: if (fcExists.Count() == 0)
97: {
98: string msg =
"MyCustomEventHandlerBehvior requires a FaultContract(typeof(MyCustomFault)) " +
99: " on each operation contract.";
100: throw new InvalidOperationException(msg);
101: }
102: }
103: }
104: }
105: }
106:
107: #endregion
108:
109:
110: private ConfigurationPropertyCollection _prop = null;
111: protected override ConfigurationPropertyCollection Properties
112: {
113: get {
114: if (this._prop == null)
115: {
116: this._prop = new ConfigurationPropertyCollection();
117: this._prop.Add(
118: new ConfigurationProperty("ServiceName", typeof(string), "",
119: ConfigurationPropertyOptions.IsRequired));
120: }
121: return this._prop;
122: }
123: }
124: }
This creates an error handler and attaches to the service behavior of the service being bound.
Now that our behavior is completed, we need a mechanism to attach it to our service via configuration or custom attribute.
For this, we need to create a class inheriting from ServiceModel.Configuration.BehaviorExtensionElement
. As per definition,
this "represents a configuration element that contains sub-elements that specify behavior extensions, which enable the user to customize service or endpoint behaviors".
Here we are configuring a service behavior as this should work with all the endpoints which the service is listening on. Following is the code:
1: using System;
2: using System.ServiceModel.Configuration;
3:
4: namespace WCFSamples.Service
5: {
6: public class MyCustomBehaviorExtensionElement : BehaviorExtensionElement
7: {
8: public override Type BehaviorType
9: {
10: get { return typeof(MyErrorHandler); }
11: }
12:
13: protected override object CreateBehavior()
14: {
15: return new MyErrorHandler();
16: }
17:
18: [System.Configuration.ConfigurationProperty("ServiceName",
19: DefaultValue = "", IsRequired = true)]
20: public string ServiceName {
21: get { return (string)base["ServiceName"]; }
22: set { base["ServiceName"] = value; }
23: }
24: }
25: }
Now with our extension ready to be binded, all it takes is a little configuration entry in the app.config file.
1: ="1.0"="utf-8"
2: <configuration>
3: <system.serviceModel>
4: <behaviors />
5: <extensions>
6: <behaviorExtensions>
7: <add name="CustomErrorHandler"
8: type="WCFSamples.Service.MyCustomBehaviorExtensionElement, WCFSamples.Service,
9: Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
10: </behaviorExtensions>
11: </extensions>
12: </system.serviceModel>
13: </configuration>
That's it. By customizing the IErrorHandler
class to log the exceptions occurring, we can identify what are the missed FaultContracts.
Then either we can provide their entries within ProvideFault or place the FaultContract attribute on the operation contract itself (better).
Happy deploying :)