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.

Donnerstag, 18. Juli 2013

Codeausschnitte im Visual Studio einbinden

Codeausschnitte kennt bestimmt jeder, ich habe ja hier in meinem Blog schon viele gepostet, die man einfach ausschneiden und in sein eigenes Programm übernehmen kann. Dabei handelt es sich meist um ganze Klassen oder komplexe Funktionen. Während des Programmierens verwendet man oft viel kleinere Codeausschnitte immer und immer wieder. Mit Hilfe der IntelliSense-Technlogie, sind diese Snippets auch recht schnell runter getippt. Beispiele gibt es viele: ein try-catch-finally-Blöcke, if-else, Properties, you name it... Und viele von uns verwenden auch die Snippet-Funktion im Visual Studio - Gibt man zum Beispiel "propfull" mit folgendem Tabulator ein, wird automatisch eine Vorlage für ein Property erstellt:
|  |
\ /

Jetzt muss man nur noch die markierten Bezeichner eingeben (Variablentyp, Feldname, Propertyname) und man hat ein Property erstellt. Das ist ja schön und gut, und auch nützlich. Aber selbst sowas machen? Oft wird die Antwort sein "Brauch ich jetzt nicht", "Da muss ich mich erst einarbeiten, da hab ich keine Zeit für".. Aber als Softwareentwickler sollten wir es doch besser wissen: Investiert man einmal ein wenig mehr Zeit, kann man in Zukunft "faul" sein.

Ich möchte Euch am Beispiel eines Snippets für ein ViewModelProperty kurz zeigen, wie es funktioniert. Zu erst benötigen wir das Code Snippet als C# Code:

private string _testField;

/// <summary>
/// gets / sets  TestProperty
/// </summary>
public string TestProperty
{
  get { 
      return _testField; 
  }
  set {
      if (value != _testField)
      {
        _testField = value;
        OnPropertyChanged(() => TestProperty);
      }
  }
}

So weit, so gut. Nachdem wir festgelegt haben, dass die Bezeichnung für das snippet propvm sein soll, müssen wir jetzt definieren, welche Teile des Codes vom Benutzer eingegeben werden, diese müssen wir dann durch ein Literal mit der Syntax $ID$ ersetzen. In unserem Fall sind das die Teile "string", "_testField" und "TestProperty":
private $type$ $field$;

/// <summary>
/// gets / sets  $property$
/// </summary>
public $type$ $property$
{
  get { 
      return $field$; 
  }
  set {
      if (value != $field$)
      {
        $field$= value;
        OnPropertyChanged(() => $property$);
      }
  }
}
Das war schon fast alles, jetzt müssen wir das ganze nur noch in das snippet-Xml-Format einfügen, ein paar Metadaten dazu und fertig sind wir. Wichtig dabei ist, dass wir jedes Literal, das wir verwenden im Xml auch deklarieren und mit einem netten Tooltip für den Anwender versehen. Das fertige Snippet sieht dann also so aus:

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
 <CodeSnippet Format="1.0.0">
  <Header>
   <Title>propvm</Title>
   <Shortcut>propvm</Shortcut>
   <Description>Codeausschnitt für eine ViewModel-Eigenschaft inkl. dem dahinter liegenden Feld</Description>
   <Author>Thomas Kison</Author>
   <SnippetTypes>
    <SnippetType>Expansion</SnippetType>
   </SnippetTypes>
  </Header>
  <Snippet>
   <Declarations>
    <Literal>
     <ID>type</ID>
     <ToolTip>Eigenschaftentyp</ToolTip>
     <Default>string</Default>
    </Literal>
    <Literal>
     <ID>property</ID>
     <ToolTip>Eigenschaftenname</ToolTip>
     <Default>MyProperty</Default>
    </Literal>
    <Literal>
     <ID>field</ID>
     <ToolTip>Die Variable hinter dieser Eigenschaft</ToolTip>
     <Default>_myVar</Default>
    </Literal>
   </Declarations>
   <Code Language="csharp">
        <![CDATA[private $type$ $field$;

/// <summary>
/// gets / sets  $property$
/// </summary>
public $type$ $property$
{
  get { 
      return $field$; 
  }
  set {
      if (value != $field$)
      {
        $field$= value;
        OnPropertyChanged(() => $property$);
      }
  }
}
 $end$]]>
   </Code>
  </Snippet>
 </CodeSnippet>
</CodeSnippets>
Jetzt müssen wir Visual Studio nur noch mitteilen, dass es dieses Script gibt. Das geht recht einfach, wir legen es in unser "Snippet-Verzeichnis" von Visual Studio. Den genauen Pfad finden wir unter Extras > Codeausschnitt-Manager unter dem Ordner My Code Snippets



Viel Spaß mit Euren eigenen Visual Studio Snippets!

Code Snippet: ViewModelBase

Oft benötige ich eine Implementierung der ViewModelBase-Klasse, dann muss ich diese entweder aus anderen Projekten kopieren, oder googeln. Jetzt muss ich das nicht mehr, denn ich merk sie mir einfach hier:

// parts of this code is based upon the article : http://msdn.microsoft.com/de-de/magazine/dd419663.aspx#id0090051
class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// raises the PropertyChanged event with the given string parameter
    /// </summary>
    /// <param name="propertyName">name of the property, that has changed</param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
        // just to avoid wrong Event-Calls (only in debug mode)
        this.VerifyPropertyName(propertyName);

        // call the event handler
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }

    /// <summary>
    /// raises the PropertyChanged-event, with the name of the property that is given by the lambda expression
    /// </summary>
    /// <typeparam name="Tprop">type parameter of the given property, to be ignored</typeparam>
    /// <param name="property">the lambda expression f.e.: "() => TestProperty"</param>
    protected virtual void OnPropertyChanged<Tprop>(Expression<Func<Tprop>> property)
    {
        // cast as LambdaExpression
        LambdaExpression lambda_exp = (LambdaExpression)property;
        if (lambda_exp != null) 
        {
            MemberExpression member_exp = (MemberExpression)lambda_exp.Body;
            if (member_exp != null)
            {
                OnPropertyChanged(member_exp.Member.Name);
                return;
            }
        }
        Debug.Fail("No correct LambdaExpression provided for OnPropertyChanged.");
    }

    [Conditional("DEBUG")]
    [DebuggerStepThrough]
    public void VerifyPropertyName(string propertyName)
    {
        // Verify that the property name matches a real,  
        // public, instance property on this object.
        if (TypeDescriptor.GetProperties(this)[propertyName] == null)
        {
            string msg = "Invalid property name: " + propertyName;
            Debug.Fail(msg);
        }
    }
}