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.