mandag den 11. juli 2011

HTTP status code in WCF using error handling

While the Web.API available on Codeplex in many ways seems like an excellent framework for building Rest-full webservices on top of WCF and .NET I recently tried to dive into the plain WCF-framework and look at the extension points available with the purpose of building a rest service directly on top of WCF.

One of the interesting and simple possibilites is to use standard WCF error handling to capture exceptions and map them to http status types. Of course this should only be a part of the exception strategy, but it is certainly usefull and makes it possible to avoid a lot of repetitive try-catch blocks. This in it self is naturally a cool feature and useful whether or not the service being implemented is aimin for rest or any other component.

The building blocks are as follows:

  • An IErrorHandler implementation that takes care of catching exceptions and perform the wanted logic operations.
  • A service behavior which injects the error handler into the WCF call stack. (Note that while we are only discussing one single error handler it would be possible to create operation behaviors and bind the exception logic by a method decorator instead.)
  • An attribute for decorating the service and binding the service behavior
  • Some kind of mapping between application exceptions and http status codes. 
Service behavior
The simplest possible example would be to map a common exception handler to all methods of a service. The error handler should be added to the ChannelDispatcher in service behavior.

This can be obtained with a class similar to the one below. It is simply an attribute that decorates the service implementation and injects an error handler of type MappingErrorHandler into the dispatcher:

namespace WcfErrorHandling
{
    using System;
    using System.ServiceModel;
    using System.ServiceModel.Description;
    using System.ServiceModel.Dispatcher;
    using System.Collections.ObjectModel;
    using System.ServiceModel.Channels;
 
    [AttributeUsage(AttributeTargets.Class)]
    public class HttpMappingErrorBehaviorAttribute
        : AttributeIServiceBehavior
    {
        public void AddBindingParameters(
            ServiceDescription serviceDescription, 
            ServiceHostBase serviceHostBase, 
            Collection<ServiceEndpoint> endpoints, 
            BindingParameterCollection bindingParameters)
        {
            // No op
        }
 
        public void ApplyDispatchBehavior(
                          ServiceDescription serviceDescription, 
                          ServiceHostBase serviceHostBase)
        {
            IErrorHandler errorHandler = new MappingErrorHandler();
            foreach (ChannelDispatcherBase channelDispatcherBase 
                  in serviceHostBase.ChannelDispatchers)
            {
                ChannelDispatcher channelDispatcher = 
                    channelDispatcherBase as ChannelDispatcher;
               channelDispatcher.ErrorHandlers.Add(errorHandler);
            }
        }
 
        public void Validate(ServiceDescription serviceDescription, 
            ServiceHostBase serviceHostBase)
        {
            // No op
        }
    }
}


The validate method should be implemented to make it more robust. In this example we assume everything is bound to the webHttpBinding so it would be wise to test if this binding was actually enabled, and of course other tests derived from the actual logics could be implemented.

IErrorHandler
Implementing the IErrorHandler interface is also very straight forward. The example below simply tests if an exception is a mapped type in a large yucky if statement and translates the two known types into http status codes. The boolean HandleError returns true if the exception is an ApplicationException; this is what this simple handler is designed to handle.


namespace WcfErrorHandling
{
    using System;
    using System.Net;
    using System.ServiceModel.Dispatcher;
    using System.ServiceModel.Web;
    using System.ServiceModel.Channels;

    
    public class MappingErrorHandler : IErrorHandler
    {
        public bool HandleError(Exception error)
        {
            return error is ApplicationException;
        }
 
        public void ProvideFault(
            Exception error, MessageVersion version
          , ref System.ServiceModel.Channels.Message fault)
        {
            if (error != null)
            {
                if (error is NotFoundException)
                {
                    SetStatus(HttpStatusCode.Conflict);
                }
                else if (error is ApplicationException)
                {
                    SetStatus(HttpStatusCode.Created);
                }
                else
                {
                    throw error;
                }
            }
        }
 
        private void SetStatus(HttpStatusCode status)
        {
            WebOperationContext.Current
             .OutgoingResponse.StatusCode = status;
            WebOperationContext.Current
             .OutgoingResponse.SuppressEntityBody = true;
        }
    }
}

Connecting to a service
To add the error handler to a service, we simply decorate the service implementation with the attribute. So assuming the service contract below

namespace WcfErrorHandling
{
    using System.ServiceModel;
    using System.ServiceModel.Web;
 
    [ServiceContract]
    public interface IExceptionThrower
    {
        [OperationContract]
        [WebGet(UriTemplate = "/hello")]
        string Hello();
 
        [OperationContract]
        [WebGet(UriTemplate="/AppError")]
        void AppError();
 
        [OperationContract]
        [WebGet(UriTemplate="/NotFound")]
        void NotFound();
    }
}


with an implementation

namespace WcfErrorHandling
{
    using System;
 
    [HttpMappingErrorBehavior]
    public class UniformCatch : IExceptionThrower
    {
        public void AppError()
        {
            throw new ApplicationException("App exception ocurred.");
        }
 
        public void NotFound()
        {
            throw new NotFoundException();
        }
 
        public string Hello()
        {
            throw new ApplicationException();
        }
    }
}

will cause all requests to be translated to status codes as described in the exception mapper.

Ingen kommentarer:

Send en kommentar