Montag, 24. Februar 2014

Generische Methoden in WCF Webservice

Manchmal wünscht man sich von einem WCF Service, dass er die Eier legende Wollmilchsau ist, die er niemal sein kann. Daher für alle Leser, die hier eine Step by Step Anleitung für das erwarten, was im Titel steht, sage ich gleich: ES GEHT NICHT! Und ich sag das nich einfach so, sondern möchte hier kurz 3 Gründe nennen, die mir spontan einfallen, warum es nicht geht. Aber erst möchte ich kurz erörtern, wozu man generische Methoden verwenden könnte, mir ist spontan nichts konkretes eingefallen, daher habe ich in der Suchmaschine meines Vertrauens, folgende Methode gefunden:

public T Get<T> Max(List<T> listOfObjects) where T: IComparable<T>

Diese Funktion gibt ein Objekt vom Typ T zurück, welches das Größte aus der gegebenen Liste ist. Klingt erstmal ganz gut, aber wenn wir in Richtung einer WCF-Implementierung denken, kommen ziemlich schnell Fragen auf, die eine Implementierung so unmöglich machen.


  1. Wie serialisieren wir den Typparameter? Schließlich soll der Service auch aus anderen Betriebssystemen / Programmiersprachen bedient werden können als C#.
  2. Interfaces werden über WCF Webservices ignoriert (im Beispiel IComparable), da sie ja nicht serialisiert/deserialisiert werden können. In gegebenem Fall beschreibt das Interfaces nur Methoden, die implementiert sein müssen, hat also nichts mit den Daten zu tun.
  3. und wenn das noch nicht überzeugt hat: Im wsdl (bzw. xsd) für den Service müssen alle Datentypen beschrieben sein, die über den Transportweg als Xml gesendet werden können. Im Fall des Parametertyps T, wären das alle Typen des .NET-Frameworks. Wer sich schonmal die Definition eines normalen WCF-Services angesehen hat, wird zugeben, dass das vermutlich den Rahmen sprengt.

Überzeugt? Dann will ich Euch gern einen Weg zeigen, wie man in einem konkreten Beispiel einen auch über Webservices ähnlichen erfolg erzielen kann. 

Das Beispiel

Nehmen wir einen Webservice, der es einem Abteilungsleiter ermöglichen soll, seine Mitarbeitergespräche zu verwalten (inkl. Vorstellungsgespräche). Dazu gibt es eine Funktion für das Seketariat, welches einen Mitarbeiter/Bewerber in eine Liste aufnimmt. In einer generischen Methode würde das so aussehen:

void AddPersonToList<T>(T person) where T:IPerson;

Für den Abteilungsleiter soll der Service eine Funktion zur Verfügung stellen, welche es ihm ermöglicht einen bestimmten Typ aus der Liste abzufragen. So ist es möglich zum Beispiel, den nächsten Bewerber zu erhalten, oder eben den nächsten Angestellten, oder gar einen speziellen Angestellten, zum Beispiel einen Teamleiter:

T GetNextPerson<T>() where T:IPerson;

Wrapper-Funktionen im ServiceContract

Ein Webservice kann intern eine generische Funktion verwenden, jedoch nicht nach außen transportieren. Eine Möglichkeit wäre also für jeden Typ eine Funktion nach außen zur Verfügung zu stellen, die entsprechend intern diese generische Methode aufruft. Je nachdem, wie viele Typen es gibt, muss eine separate Funktion zur Verfügung gestellt werden:

// secretary functions
public void AddApplicant(Applicant person)
{
   AddPersonToList<Applicant>(person);
}
public void AddEmployee(Employee person) {..}
public void AddSapEmployee(SapEmployee person) {..}
public void AddNetDeveloper(NetDeveloper person) {..}
[..]

// manager functions
public Applicant GetNextApplicant()
{
   return GetNextPerson<Applicant>();
}

public Employee GetNextEmployee() {..}
public SapEmployee GetNextSapEmployee() {..}
public NetDeveloper GetNextNetDeveloper() {..}
[..]
Für den ersten Schuss ist die Idee ganz gut, aber mit Sicherheit nicht ideal, zumindest muss man dadurch keine Code-Dubletten schreiben, sondern kann die bestehende Implementierung verwenden.

Verwendung der Basistypen im OperationContract

Wie man schnell sieht, implementieren in unserem Fall alle Personen das Interface IPerson. Unsere erste generische Funktion können wir also unter Verwendung des Interface etwas generalisieren:

void AddPersonToList(IPerson person);

In diesem Fall entspricht der Funktionsaufruf syntaktisch sogar dem Original. Es gibt aber immer noch ein Problem, denn wir wissen nicht, welche Implementierungen von IPerson in Frage kommen. Damit kann der Webservice auch keine valide Schnittstellenbeschreibung für eine mögliche Clientimplementierung zur Verfügung stellen. Um genau zu sein, würde es so erst zur Laufzeit zu einem Fehler kommen. Ruft man zum Beispiel in der Clientanwendung AddPersonToList(new SapDeveloper()) auf, bekommt man folgenden Fehler:





Der Fehlerdetails an sich erschließen sich einem im ersten Moment nicht:
Zusätzliche Informationen: Fehler beim Deserialisieren von Parameter http://tempuri.org/:person. Die InnerException-Nachricht war "Der Typ 'Test.Model.SapDeveloper' mit dem Datenvertragsnamen 'SapDeveloper:http://schemas.datacontract.org/2004/07/Test.Model' wird nicht erwartet. Verwenden Sie ggf. einen DataContractResolver, oder fügen Sie alle unbekannten Typen statisch der Liste der bekannten Typen hinzu, beispielsweise mithilfe des KnownTypeAttribute-Attributs oder indem Sie sie zur Liste der bekannten Typen hinzufügen, die an DataContractSerializer übergeben wird.".  Weitere Details finden Sie unter "InnerException".


Der Client könnte theoretisch ein Objekt der Klasse SapDeveloper serialisieren, dies würde sogar der Schnittstellenbeschreibung entsprechen, denn der Soap-Service definiert den Übergabeparameter als anyType


Jedoch gibt die Schnittstellenbeschreibung nicht wirklich vor, welche Implementierung verwendet werden soll, bzw. welche denn der Webservice verstehen würde. Auch wenn wir es also schaffen würden, die Serialisierung des Objekts der Klasse SapDeveloper auf Client Seite hinzubekommen, könnte der Server damit nicht viel anfangen. 

Der Akteur ist also immer derjenige, der die Schnittstelle vorgibt - also der Webservice. Aber die Fehlermeldung gibt noch mehr Anhaltspunkte, wie wir weiter machen können,es gibt ein KnownType-Attribut, welches man verwenden kann, um eine Liste möglicher Implementierungen anzugeben. Dieses Attribut wird verwendet, um einem DataContractSerializer zu sagen, welche Implementierungen dieser serialisieren bzw. deserialisieren "darf". Leider verwenden wir den DataContractSerializer nur implizit, denn das macht der WCF Service für uns. Daher gibt es das Attribut ServiceKnownType, welches man direkt am Service-Interface definieren kann, um mögliche Implementierungen Service-weit zu definieren. In unserem Fall sieht die fertige Servicedefinition so aus:


    [ServiceKnownType(typeof(Test.Model.Person))]
    [ServiceKnownType(typeof(Test.Model.Employee))]
    [ServiceKnownType(typeof(Test.Model.SAPEmployee))]
    //[ServiceKnownType("GetKnownTypes", typeof(Helper))]
    [ServiceContract]
    public interface IService1
    {
        [OperationContract]
        IPerson GetNextInterviewPartner();

        
        // so geht das nicht
        //[OperationContract]
        //T GetNextInterviewPartnerGeneric() where T : IPerson;

        [OperationContract]
        IPerson GetNextInterviewPartner(string typeFullName);

        [OperationContract]
        void AddPersonToList(IPerson person);

     
    }
Wie man leicht sieht, gibt es zwei Varianten das Attribut zu verwenden, entweder man gibt die Typen direkt hintereinander an, oder einen Verweis auf eine Methode, die ein IEnumerable<Type> zurückgibt.

Eigentlich ist damit die Aufgabe gelöst, nur gibt es einen unschönen Nebeneffekt. Da es in der Schnittstellenbeschreibung keine Interfaces gibt, werden diese immer als anyType interpretiert, in einem C#-Stub ist dies dann der Typ object. Das führt dazu, dass jede Art von Typisierungsfehlern erst zur Laufzeit auffallen. Um das einzudämmen, sollte man stattdessen entweder mit einer Basisimplementierung als Parametertyp arbeiten oder eine abstrakte Klasse zur Schnittstellenbeschreibung verwenden.