Handling exceptions the right way in WCF (part 2)
In my previous post I talked about FaultExceptions and how they are transmitted from the server to the client.
First, let's have a look at the sample service contract :
Now we'll learn how to ease the process of creating FaultExceptions by using the IErrorHandler interface. This interface allows the customization of exceptions and provide a centralized location for responding to exceptions in a very generic way.
This interface contains two methods :
The following code is a simple implementation of the IErrorHandler interface. It simply create a new FaultException and send it to the caller.
The implementation here is really simple, you will probably need to add some logic in the ProvideFault method to distinguish between technical exception and business exceptions and create the appropriate type of fault.
The next step is to associate this error handler with the service. Each ChannelDispatcher of a ServiceHost have a property named ErrorHandlers that contains a list of error handlers applied to the channel. One easy way to add a error handler to the channel dispatcher is to create a ServiceBehavior. To create a new service behavior, we simply need to implement IServiceBehavior in the TimeServiceBehavior class :
The only method of interest here is ApplyDispatchBehavior used to apply a specific behavior on the dispatchers (which is exactly what we are trying to achieve here).
Unfortunately, in its current state, our service behavior is not easy to use unless we are creating the service host and all its configuration by hand. For example, it is not possible to use our behavior from a configuration file.
Let's make this service behavior a little more developer friendly. We have two possible solutions :
The code is pretty straight-forward, it just links our service behavior to a configuration element.
Here is an example of a configuration file making use of the TimeServiceBehavior :
That's it for the configuration model. Now let's make the behavior a little easier to use from a developer perspective. We are simply going to make the service behavior a custom attribute. This attribute will then be applied on the service itself. To transform the behavior into an attribute, TimeServiceBehavior will now inherit from Attribute, that's it :
Now, the service implementation is decorated with our attribute :
This will have exactly the same effect than the configuration based implementation.
You can implement both solutions for your behaviors and have the choice of using one way or another depending on your policy ("all in config", "fixed service behavior", etc.).
First, let's have a look at the sample service contract :
[ServiceContract]
public interface ITimeService
{
[OperationContract]
[FaultContract(typeof(TimeExceptionDetails))]
DateTime GetTime();
}
The service implementation :public interface ITimeService
{
[OperationContract]
[FaultContract(typeof(TimeExceptionDetails))]
DateTime GetTime();
}
public class TimeService : ITimeService
{
#region ITimeService Members
public DateTime GetTime()
{
return DateTime.Now;
}
#endregion
}
And the Fault details class :{
#region ITimeService Members
public DateTime GetTime()
{
return DateTime.Now;
}
#endregion
}
public class TimeExceptionDetails
{
public static readonly TimeExceptionDetails Default = new TimeExceptionDetails();
[DataMember]
public String Message { get; private set; }
public TimeExceptionDetails()
{
this.Message = "There is a problem in the spacetime continuum, Marty";
}
public TimeExceptionDetails(String message)
{
this.Message = message;
}
}
{
public static readonly TimeExceptionDetails Default = new TimeExceptionDetails();
[DataMember]
public String Message { get; private set; }
public TimeExceptionDetails()
{
this.Message = "There is a problem in the spacetime continuum, Marty";
}
public TimeExceptionDetails(String message)
{
this.Message = message;
}
}
Now we'll learn how to ease the process of creating FaultExceptions by using the IErrorHandler interface. This interface allows the customization of exceptions and provide a centralized location for responding to exceptions in a very generic way.
This interface contains two methods :
- HandleError
- ProvideFault
The following code is a simple implementation of the IErrorHandler interface. It simply create a new FaultException and send it to the caller.
public class TimeServiceBehavior : IErrorHandler
{
#region IErrorHandler Members
public bool HandleError(Exception error)
{
// Log the error here
EventLog.WriteEntry("TimeService", error.ToString());
// Let the other ErrorHandler do their jobs
return true;
}
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
if (error is FaultException)
return;
// Creates the exception we want to send back to the client
var exception = new FaultException<TimeExceptionDetails>(
TimeExceptionDetails.Default,
new FaultReason(TimeExceptionDetails.Default.Message));
// Creates a message fault
var messageFault = exception.CreateMessageFault();
// Creates the new message based on the message fault
fault = Message.CreateMessage(version, messageFault, exception.Action);
}
#endregion
}
{
#region IErrorHandler Members
public bool HandleError(Exception error)
{
// Log the error here
EventLog.WriteEntry("TimeService", error.ToString());
// Let the other ErrorHandler do their jobs
return true;
}
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
if (error is FaultException)
return;
// Creates the exception we want to send back to the client
var exception = new FaultException<TimeExceptionDetails>(
TimeExceptionDetails.Default,
new FaultReason(TimeExceptionDetails.Default.Message));
// Creates a message fault
var messageFault = exception.CreateMessageFault();
// Creates the new message based on the message fault
fault = Message.CreateMessage(version, messageFault, exception.Action);
}
#endregion
}
The implementation here is really simple, you will probably need to add some logic in the ProvideFault method to distinguish between technical exception and business exceptions and create the appropriate type of fault.
The next step is to associate this error handler with the service. Each ChannelDispatcher of a ServiceHost have a property named ErrorHandlers that contains a list of error handlers applied to the channel. One easy way to add a error handler to the channel dispatcher is to create a ServiceBehavior. To create a new service behavior, we simply need to implement IServiceBehavior in the TimeServiceBehavior class :
public class TimeServiceBehavior : IErrorHandler, IServiceBehavior
{
#region IErrorHandler Members
[...]
#endregion
#region IServiceBehavior Members
public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
{
}
public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
// Adds a TimeServiceErrorHandler to each ChannelDispatcher
foreach (var channelDispatcherBase in serviceHostBase.ChannelDispatchers)
{
var channelDispatcher = channelDispatcherBase as ChannelDispatcher;
channelDispatcher.ErrorHandlers.Add(new TimeServiceBehavior());
}
}
public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
}
#endregion
}
{
#region IErrorHandler Members
[...]
#endregion
#region IServiceBehavior Members
public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
{
}
public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
// Adds a TimeServiceErrorHandler to each ChannelDispatcher
foreach (var channelDispatcherBase in serviceHostBase.ChannelDispatchers)
{
var channelDispatcher = channelDispatcherBase as ChannelDispatcher;
channelDispatcher.ErrorHandlers.Add(new TimeServiceBehavior());
}
}
public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
}
#endregion
}
The only method of interest here is ApplyDispatchBehavior used to apply a specific behavior on the dispatchers (which is exactly what we are trying to achieve here).
Unfortunately, in its current state, our service behavior is not easy to use unless we are creating the service host and all its configuration by hand. For example, it is not possible to use our behavior from a configuration file.
Let's make this service behavior a little more developer friendly. We have two possible solutions :
- Make it compatible with the file-based configuration infrastructure (app/web.config),
- Make it an attribute that can decorate our service implementation.
public class TimeServiceBehaviorElement : BehaviorExtensionElement
{
public override Type BehaviorType
{
get { return typeof(TimeServiceBehavior); }
}
protected override object CreateBehavior()
{
return new TimeServiceBehavior();
}
}
{
public override Type BehaviorType
{
get { return typeof(TimeServiceBehavior); }
}
protected override object CreateBehavior()
{
return new TimeServiceBehavior();
}
}
The code is pretty straight-forward, it just links our service behavior to a configuration element.
Here is an example of a configuration file making use of the TimeServiceBehavior :
<system.serviceModel>
<services>
[...]
</services>
<behaviors>
<serviceBehaviors>
<behavior name="WcfErrorSample.Service1Behavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
<timeService />
</behavior>
</serviceBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add name="timeService" type="WcfErrorSample.TimeServiceBehaviorElement,
WcfErrorSample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
</system.serviceModel>
<services>
[...]
</services>
<behaviors>
<serviceBehaviors>
<behavior name="WcfErrorSample.Service1Behavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
<timeService />
</behavior>
</serviceBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add name="timeService" type="WcfErrorSample.TimeServiceBehaviorElement,
WcfErrorSample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
</system.serviceModel>
That's it for the configuration model. Now let's make the behavior a little easier to use from a developer perspective. We are simply going to make the service behavior a custom attribute. This attribute will then be applied on the service itself. To transform the behavior into an attribute, TimeServiceBehavior will now inherit from Attribute, that's it :
public class TimeServiceBehavior : Attribute, IErrorHandler, IServiceBehavior
{
[...]
}
{
[...]
}
Now, the service implementation is decorated with our attribute :
[TimeServiceBehavior]
public class TimeService : ITimeService
{
[...]
}
public class TimeService : ITimeService
{
[...]
}
This will have exactly the same effect than the configuration based implementation.
You can implement both solutions for your behaviors and have the choice of using one way or another depending on your policy ("all in config", "fixed service behavior", etc.).
Great post :)
ReplyDeletehmm.. simple and to the point.
ReplyDeleteWow, Johann. I have been searching the entire day trying to find an easy to follow and GOOD example of how this works. I am a WCF noob and am at work trying to figure out the best way to handle exceptions and wow this example made my day.
ReplyDeleteThanks a ton from Hyderabad, India!
ReplyDelete