2009. december 3., csütörtök

WCF exception faultá alakítása háziassszonyoknak

Adott a szituáció, hogy van egy üzleti logia / üzleti folyamat rétegünk, amiből szeretnénk néhány dolgot publikálni a külvilág felé - ehhez a WCF a választott technológiánk. Néhány dolgot nem publikálunk, azokat csak a service réteget assemblyként referenciáló kódok érik el (ők is a sajátjaink, a rendszer többi komponense).
A WCF remek választás, hisz pont erről szól: úgy implementálhatod a service rétegedet, hogy arra koncentrálsz, amit csinálni akarsz - azzal meg nem kell törődnöd, hogy hogy lesz ez a szolgáltatás távolról elérhető. Nem kell a kommunikációval foglalkozó kódot írnod, csak felpattintasz néhány attribútumot (, átfusz néhány ezer sor XML-t), és kész. Eddig hurrá.

A feketeleves ott kezdődik, hogy az üzleti folyamatok végrehajtása nem mindig sikeres (akár azért, mert nem álltunk a helyzet magaslatán, mikor implementáltunk, akár azért, mert valami külső körülmény - pl. egy elhalálozott adatbázisszerver - miatt nem tudjuk teljesíteni kliensünk kérését).
Vegyük pl. a jó öreg számológép szolgáltatást - jelen esetben a megvalósításunk, a FawltyCalculator kicsit bugos. A metódusunk hívása egy kövér DivideByZeroException-t fog dobni.

[ServiceContract]

interface ICalculator

{

    [OperationContract]

    int Divide(int dividend, int divisor);

}

 

public class FawltyCalculator : ICalculator

{

    public int Divide(int dividend, int divisor)

    {

        int result;

        result = dividend / (divisor * 0); //buggy

        return result;

    }

}

A dolgok ilyentén való félremenésére a kliensoldalon is fel kell készülni. A WCF megteszi nekünk azt a szivességet, hogy a szolgáltatások futása közben dobott, kezeletlen kivételeket szépen megeszi, és a szervíz hívásra küldött válaszban jelzi, hogy baj van (volt).
A túloldalon (a kliensnél) ez egy FaultException képében fog materializálódni. Ott tehát nem egy DivideByZeroException elkapására kell felkészülni, hanem egy FaultException-ére, amivel egy baj lesz: semmit nem fogunk tudni arról, hogy mi is ment félre, kifutottunk a memóriából, nullával osztottunk, vagy kihalt alólunk valami egyéb szoftverkomponens. Megtehetjük ugyan, hogy arra instruáljuk a WCF-et, hogy adja vissza a kliensoldalnak a részletes exception-t, ami development-time hasznos feature, de produkciós környezetben nem túl szerencsés (egyrészt nem túl user friendly megoldás, másrész túl sok mindent köthetünk így a klienseink orrára, amit esetleg nem szeretnénk).

A megoldás: dobjunk magunk is FaultException-t, már a service oldali kódban! Annak megadhatunk FaultReason-t, FaultCode-ot, az szépen át fog menni a kliens oldalra:

    public int Divide(int dividend, int divisor)

    {

        int result;

        try

        {

            result = dividend / (divisor * 0); //buggy

        }

        catch (DivideByZeroException)

        {

            throw new FaultException(

                new FaultReason("A nullával való osztás nem menő."),

                new FaultCode("DIVIDE_BY_ZERO")

                );

        }

        return result;

    }

Ez eddig remek. Mi vele a gond? Az, hogy a WCF azt ígérte nekünk, hogy úgy kódolhatunk szolgáltatást, hogy közben nem kell ilyesmivel törődni. Márpedig ha a Divide() metódus nem egy WCF-en át elérhető szolgáltatás lenne, hanem csak úgy meghívnánk kódból, akkor még úgy is nehezen indokolható a fenti try-catch blokk, ha nem véletlenül vagyunk mi a FawltyCalculator dedikált fejlesztői.
Mi történik itt? Ez az absztrakció szivárog.
Sőt, ez inkább már folyik.

Az lenne a jó, ha nem kéne FaultException-t dobnunk, de mégis, akkor, és csakis akkor, ha a metódunkat WCF-en keresztül, távolról hívják, valahogy mégis úgy legyen minden, minthacsak azt dobtunk volna.
Szerencsére van megoldás, úgy hívják, hogy IErrorHandler (a System.ServiceModel.Dispatcher névtér alatt lakik). Nem túl meglepő módon ez egy interface, ami két metódus megvalósítását írja elő: a HandleError()-ban a szervízoldalon dobott, kezeletlen kivétellel kezdhetünk valamit (logging, alkalmazás meghalasztása, stb.), a ProvideFault() pedig pont az, ami nekünk kell: az exception ismeretében megkonstruálhatjuk a kliensünknek adandó választ. Ezt akár bit (na jó: XML element) szinten is kontrollálhatjuk, de ha lustábbak vagyunk, rábízhatjuk a Frameworkre is a legyártását:

public void ProvideFault(Exception error, System.ServiceModel.Channels.MessageVersion version, ref Message fault)

{

    FaultException faultException = null;

 

    if (error is ApplicationException)

    {

        faultException = new FaultException(

            new FaultReason(string.Format("Alkalmazáshiba történt ('{0}').", error.Message)),

            new FaultCode("APPLICATION_ERROR")

            );

    }

    else if (error is System.Data.Linq.ChangeConflictException)

    {

        faultException = new FaultException(

            new FaultReason(string.Format("Több felhasználó próbálta párhuzamosan módosítani ugyanazt az adatot.")),

            new FaultCode("DATABASE_CONCURRENCY_ERROR")

        );

    }

 

    // Still unhandled - provide some default...

    if (faultException == null)

    {

        faultException = new FaultException(

            "Kezeletlen kivétel történt.",

            new FaultCode("UNHANDLED_ERROR"));

    }

 

    var messageFault = faultException.CreateMessageFault();

 

    // ref Message fault

    fault = Message.CreateMessage(

        version,

        messageFault,

        "http://www.w3.org/2005/08/addressing/soap/fault"

        );

}


[Felhívnám a figyelmet a "http://www.w3.org/2005/08/addressing/soap/fault" sorra, azt magában fél nap volt kififikázni.]

Ezek után már csak rá kell vennünk a WCF-et, hogy használja is az errorhandlerünket.
Ehhez készítenünk kell egy osztályt, ami implementálja az IServiceBehavior interfészt. Ennek az osztálynak a ApplyDispatchBehavior() metódusában fogjuk az ErrorHandlerünket befűzni a WCF folyamatába. Ezután három út van: a) programozottan bepéldányosítjuk a service behaviorunkhat, és megetetjük a service host objektummal, b) az IServiceBehavior megvalósításon kívül leszármazunk az Attribute-ból is, és attribútumként felpattintjuk a szervizünkre c) az IServiceBehavior megvalósításon kívül leszármazunk a BehaviorExtensionElement-ből, és az app.configban konfiguráljuk az error handlingot. (Ez mekkora!)
Én a b)-t választottam:

public sealed class ErrorBehaviorAttribute : Attribute, IServiceBehavior

{

    private Type _typeErrorHandler;

 

    public ErrorBehaviorAttribute(Type typeErrorHandler)

    {

        _typeErrorHandler = typeErrorHandler;

    }

 

 

    public void Validate(ServiceDescription description, ServiceHostBase serviceHostBase) { }

    public void AddBindingParameters(ServiceDescription description, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection parameters) { }

 

    private IErrorHandler CreateTypeHandler()

    {

        var typeErrorHandler = (IErrorHandler)Activator.CreateInstance(_typeErrorHandler);

        return typeErrorHandler;

    }

 

    public void ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase)

    {

        IErrorHandler typeErrorHandler = this.CreateTypeHandler();

        foreach (var channelDispatcher in serviceHostBase.ChannelDispatchers)

        {

            (channelDispatcher as ChannelDispatcher).ErrorHandlers.Add(typeErrorHandler);

        }

    }

}


Ennyi. Megy. Persze lehet még tovább cifrázni, pl. hosszú távon a ProvideError() jó eséllyel hízik túl minden vállalható méreten, de ez már egy másik sztori.
Mára ennyit!

2 megjegyzés:

Érsek Attila írta...

Szia!

Ez nálam is probléma volt, de az IErorrHandler sajnos nem 100%-os megoldás. Elég csak ránézni a szolgáltatásod wsdl-jére, sehol semmi nyoma egyetlen faultcontractnak sem. Ugye? A kliens nem fog tudni róla, hogy te ilyet küldhetsz.

Ezt viszonylag egyszerű megoldani, ha kiegészíted a megoldásod egy Attribute,IContractBehavior implementációval, ahol a lényeg, hogy a Validate metódusban kell felpakolni a az operationdescription-be a megfelelő faultokat.

Egy komment nem hely erre, így szerintem majd dobok egy blogbejegyzést rá ha érdekes...

Molnár Gergő írta...

Szia,
a kliensen nem is kell ez esetben felkészülni semmire, mert "sima" FaultException-ök mennek vissza. FaultContract-ot akkor kell használnom, ha valamilyen FaultException<T>-t akarok dobni, nem? Vagy valamit nem értek. :)
Mindenesetre a IContractBehavior-nak, amit írtál, utánanézek.

Megjegyzés küldése