J’ai récemment voulu mettre en place une gestion d’exceptions « WCF-compliant » sur un service WCF.

L’idée étant la suivante : le service WCF doit attraper toutes les BusinessException susceptibles d’être levées par la couche métier et les transformer en FaultException<BusinessFault>

J’ai donc mis en place un handler d’exception qui implémente l’interface IErrorHandler ainsi qu’un « comportement de service » (une classe implémentant IServiceBehavior en fait).

Parallèlement à ça j’ai défini un contrat « BusinessFault » et fait en sorte que mes exceptions métier (BusinessException) puissent fournir la BusinessFault correspondante.

 

Voici le code de la BusinessException :

 

public class BusinessException : Exception

{

    public void TranslateToFault(MessageVersion version, ref Message faultMessage)

    {

        string inner = (InnerException == null ? "" : InnerException.GetType().Name);

        BusinessFault tf = new BusinessFault(this.Code, this.Message, inner);

        MessageFault f = MessageFault.CreateFault(new FaultCode("Receiver"), new FaultReason("Erreur métier"), tf);

        erreur = System.ServiceModel.Channels.Message.CreateMessage(version, f, "defaultAction");

    }

 

}

 

BusinessFault est une simple classe marquée comme [DataContract] et possédant 3 accesseurs de type string marqués comme [DataMember].

 

Ci-après le code du handler d’exception :

 

public sealed class MyExceptionHandler : IErrorHandler

{

    bool IErrorHandler.HandleError(Exception ex)

    {

        return true;

    }

 

    void IErrorHandler.ProvideFault(Exception ex, MessageVersion version, ref Message fault)

    {

        BusinessException exception = ex as BusinessException;

        if (exception != null)

        {

            exception.TranslateToFault(version, ref fault);

        }

        else

        {

            HandleUnknownException(ex, version, ref fault);

        }

    }

 

    private void HandleUnknownException(Exception exception, MessageVersion version, ref Message erreur)

    {

        // ...

    }

}

 

Et maintenant le behavior qui permet de transformer automatiquement toutes les BusinessExceptions en FaultException :

 

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]

public sealed class MyExceptionHandlingBehaviorAttribute : Attribute, IServiceBehavior

{

    void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)

    {

    }

 

    void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)

    {

        if (serviceHostBase == null)

        {

            return;

        }

 

        // Ajout du handler d'exception sur tous les dispatchers WCF

        foreach (ChannelDispatcher dispatcher in serviceHostBase.ChannelDispatchers)

        {

            dispatcher.ErrorHandlers.Add(new MyExceptionHandler());

        }

    }

 

    void IServiceBehavior.Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)

    {

    }

}

 

 

Pour que tout ceci fonctionne bien, il restait à décorer les méthodes de service comme suit :

 

[FaultContract(typeof(BusinessFault)]

PersonneDataContract EnregistrerPersonne(PersonneDataContract personneDataContract);

 

Et enfin décorer la classe d’implémentation de service avec l’attribut précédemment créé :

 

[MyExceptionHandlingBehaviorAttribute]

public class MyService : IMyService

{

}

 

Et voilà. “Normalement” tout doit fonctionner…

En effet ca fonctionne bien… tant que le endpoint de mon service utilise du basicHttpBinding !

Dès que l’on passe en wsHttpBinding, ca ne fonctionne plus, en effet coté client au lieu de récupérer une FaultException<BusinessFault>, je récupère une CommunicationException, bof bof !

Pour corriger cela, il est en fait nécessaire de spécifier une action sur la fault fournie par le service et il faut aussi spécifier cette même action sur les méthodes de service. Ainsi il suffit de définir une constante FaultAction, par exemple :

 

public class Constantes

{

    public const string MyFaultAction = "http://MyService/Fault";

}

 

Ensuite il faut modifier notre méthode TranslateToFault (sur la classe BusinessException) comme ceci :

 

public void TranslateToFault(MessageVersion version, ref Message faultMessage)

{

    string inner = (InnerException == null ? "" : InnerException.GetType().Name);

    BusinessFault tf = new BusinessFault(this.Code, this.Message, inner);

    MessageFault f = MessageFault.CreateFault(new FaultCode("Receiver"), new FaultReason("Erreur métier"), tf);

    erreur = System.ServiceModel.Channels.Message.CreateMessage(version, f, Constantes .MyFaultAction);

}

 

Notez l’utilisation de la constante dans l’appel de la méthode Message.CreateMessage, pour le paramètre « Action ».

 

Enfin il reste à spécifier cette action sur la méthode de service qui devient donc :

 

[FaultContract(typeof(BusinessFault), Action = Constantes.MyFaultAction)]

PersonneDataContract EnregistrerPersonne(PersonneDataContract personneDataContract, ErrorManager errorManager);

 

Une fois régénéré le proxy coté client, ce dernier est maintenant en mesure de catcher correctement des FaultException<BusinessFault> levées par le service.