Table of Contents

REST Endpoint für Service-Methoden

Standardmäßig bietet ein Service Host den ausgewählten Service bzw. dessen Service Contract nach außen hin über WCF und damit das SOAP-Protokoll an. Moderne Applikationen und allen voran Webapplikationen in Borwsern oder auf mobilen Endgeräten unterstützen mittlerweile jedoch oft kein SOAP mehr und bevorzugen deshalb die Kommunikation über JSON.

Framework Studio bietet die Möglichkeit, den Service Host so zu konfigurieren, dass vollständig automatisch ein zusätzlicher Endpoint generiert wird, mit dem die im Service Contract propagierten Methoden auch über JSON und damit über einen normalen HTTP-Request angesprochen werden können.

Um die vollständige Automatisierung dieser Funktionalität gewährleisten zu können, bedarf es einiger Regeln, die beachtet werden müssen. Diese betreffen vor allem die Kommunikation zwischen dem Client und dem generierten REST Endpoint des Service Hosts.

Aktivieren des REST-Endpoints

Der Service Host generiert den REST Endpoint, wenn die Checkbox Generate REST Endpoint aktiviert wird.

Service Host REST-Endpoint

In der Konfigurationsdatei des Servic Hosts wird dadurch ein neuer Endpoint mit dem ausgewählten Contract registriert:

<endpoint name="rest" address="api" binding="webHttpBinding" contract="App.IHelloServiceContract" />
Warning

Der REST Endpoint basiert auf der in WCF integrierten webHttpBinding. In einem WCF-Service darf für einen Service Contract nur ein Binding eines bestimmten Typs registriert werden. Sollte am Service Host schon ein Endpoint mit einem webHttpBinding für den ausgewählten Contract existieren, so kann das automatische Generieren des REST Endpoints nicht verwendet werden bzw. wird beim Start des Service Host zu Fehlern führen.

Tip

Der REST-Endpoint kann alternativ auch individuell definiert werden, wenn die Standard-Konfiguration den Anforderungen nicht genügt.

Der Endpoint hat fest den Namen api und wird immer auf der im Service Host angegebenen Base Address registiert.

Ist die Base Address z.B.

http://localhost:1234/HelloServiceHost

dann wird der REST Endpoint unter

http://localhost:1234/HelloServiceHost/api

zu finden sein.

Im Development Service ist der neue Endpoint auch in der Auflistung im System Tray zu sehen:

DevService Endpoints

Aufrufen von Service-Methoden am REST Endpoint

Das Aufrufen einer durch den REST Endpoint bereitgestellten Service Methode erfolgt über einen normalen HTTP-Request. Dabei gibt es einige Regeln, die beachtet werden müssen. Nachfolgend werden alle Spezialitäten der Kommunikation detailliert beschrieben und mit mehreren Beispielen transparent dargestellt.

In den Beispielen wird ein Service namens "HelloService" verwendet. Dieser besteht aus 4 Methoden, die den Benutzer auf unterschiedliche Weise grüßen.

Service Host Methoden

Separate Urls für Service-Methoden

Um die Methoden am REST Endpoint voneinander unterscheiden zu können, besitzt jede Methode eine eigene Url. Ist der REST Endpoint beispielsweise unter

http://localhost:1234/HelloServiceHost/api

registriert, so werden die einzelnen Methoden wie folgt angesprochen:

Methodenname Url
SayHello http://localhost:1234/HelloServiceHost/api/sayhello
SayHelloComplex http://localhost:1234/HelloServiceHost/api/sayhellocomplex
SayHelloFault http://localhost:1234/HelloServiceHost/api/sayhellofault
SayHelloName http://localhost:1234/HelloServiceHost/api/sayhelloname

Der Methodenname wird also als Url-Erweiterung am REST Endpoint genutzt. Auf Groß- und Kleinschreibung muss an dieser Stelle nicht geachtet werden.

Um testweise HTTP-Requests auf den Service Host absetzen zu können, wird in den nachfolgenden Beispielen die App Postman genutzt. Sie ist kostenlos unter https://www.postman.com erhältlich.

Postman

Beispiel 1: Parameterlose Methoden

public virtual string SayHello()
{
    return "Hello!";
}

Am REST Endpoint ist diese Methode unter http://localhost:1234/HelloServiceHost/api/sayhello aufrufbar.

Parameterlose Methoden können ausschließlich mit HTTP GET aufgerufen werden.

Die Header Content-Type und Content-Encoding müssen nicht zwingend gesetzt sein, da bei parameterlosen Methoden im Request kein HTTP-Content übertragen wird.

Der Header Accept muss auf application/json gesetzt sein oder diesen MIME-Type beinhalten (wird im Beispiel durch */* abgedeckt), ansonsten führt der Response am Client zu einem Fehler.

Beispiel 1

Der Response hat ausschließlich den Content-Type application/json und das Content-Encoding utf-8. Somit kann der Response ohne Umwege z.B. vom im JavaScript integrierten JSON-Parser eingelesen werden.

Beispiel 2: Methoden mit Parametern (einfache Datentypen)

public virtual string SayHelloName(string name)
{
    return "Hello " + name + "!";
}

Am REST Endpoint ist diese Methode unter http://localhost:1234/HelloServiceHost/api/sayhelloname aufrufbar.

Methoden mit Parametern müssen zwinged mit HTTP POST aufgerufen werden.

Der Header Content-Type muss zwingend auf application/json gesetzt sein, da der Service Host eine JSON-Kommunikation erwartet.

Um Encoding-Fehlern vorzubeugen, wird empfohlen, den Header Content-Encoding entsprechend des übertragenen Contents zu setzen (vorzugsweise utf-8).

Der Header Accept muss auf application/json gesetzt sein oder diesen MIME-Type beinhalten (wird im Beispiel durch */* abgedeckt), ansonsten führt der Response am Client zu einem Fehler.

Methodenparameter müssen im HTTP-Content in einem JSON-Objekt übertragen werden. Url-Parameter werden nicht unterstützt. Jeder Methodenparameter wird als JSON-Property mit dem exakten Namen (Groß- und Kleinschreibung beatchen) übergeben.

Im Beispiel wäre das der Parameter string name. Der Parameter wird im JSON wie folgt übertragen:

{
    "name": "Max Mustermann"
}

Beispiel 2

Der Response hat ausschließlich den Content-Type application/json und das Content-Encoding utf-8. Somit kann der Response ohne Umwege z.B. vom im JavaScript integrierten JSON-Parser eingelesen werden.

Hat eine Methode mehrere Parameter, so werden diese im JSON-Objekt ebenfalls als Properties hinzugefügt. Die Reihenfolge ist dabei nicht relevant.

public virtual string SayHelloMultiple(string name, int age, string city)

Diese Methode würde folgendes JSON-Objekt wie gewünscht verarbeiten, obwohl die Parameter vertauscht sind:

{
    "name": "Max Mustermann",
    "city": "Stockach",
    "age": 36
}

Beispiel 3: Methoden mit Parametern (komplexe Datentypen)

public virtual dcHelloResult SayHelloComplex(dcHelloInput input)
{
    dcHelloResult result = new dcHelloResult()
    {
        sGreeting = "Welcome!",
        sFirstName = input.sFirstName,
        sLastName = input.sLastName
    };
    return result;
}

Für einen Methodenaufruf mit komplexen Datentypen wie z.B. DataContracts oder Arrays gelten dieselben Regeln wie für einen Methodenaufruf mit einfachen Datentypen. Der einzige Unterschied ist, dass der jeweilige Parameter als JSON-Objekt übertragen wird.

Im Beispiel hat die Methode einen Parameter vom Typ dcHelloInput mit zwei Properties. Dieser DataContract sieht im Framework Studio folgendermaßen aus:

dcHelloInput

Als Rückgabetyp dient der DataContract dcHelloResult mit drei Properties, welcher so definiert ist:

dcHelloResult

Der Aufruf der Service-Methode gestaltet sich intuitiv wie folgt:

Beispiel 3

Hat eine Methode mehrere komplexe Parameter, so gilt dieselbe Nomenklatur wie in Beispiel 2.

public virtual string SayHelloComplexMultiple(
    dcHelloInput input, dcHelloInput inputOther, string greeting)
{
    return $"{greeting} {input.sFirstName} {input.sLastName}! "
        + $"{greeting} {inputOther.sFirstName} {inputOther.sLastName}!";
}

Diese Methode würde folgendes JSON-Objekt wie gewünscht verarbeiten:

{
    "input": {
        "sFirstName": "Max",
        "sLastName": "Mustermann"
    },
    "inputOther": {
        "sFirstName": "Peter",
        "sLastName": "Pan"
    },
    "greeting": "Hi"
}

Beispiel 4: Exceptions und FaultContracts

public virtual void SayHelloFault()
{
    dcFault fault = new dcFault()
    {
        sProp1 = "Fault Property 1",
        sProp2 = "Fault Property 2"
    };
    throw new FaultException<dcFault>(fault, "OH NO!", new FaultCode("Custom Fault Code"), "Custom Fault Action");
}

Tritt währende der Verarbeitung im Service eine Exception auf oder wird aus der Logik heraus eine FaultException geworfen, so werden auch diese im JSON an den Client übertragen. Die FaulException aus obigem Code stellt sich im JSON folgendermaßen dar:

{
    "Action": "Custom Fault Action",
    "Code": {
        "IsPredefinedFault": true,
        "IsSenderFault": false,
        "IsReceiverFault": false,
        "Namespace": "",
        "Name": "Custom Fault Code",
        "SubCode": null
    },
    "Reason": {
        "Translations": [
            {
                "XmlLang": "en-US",
                "Text": "OH NO!"
            }
        ]
    },
    "Detail": {
        "sProp2": "Fault Property 2",
        "sProp1": "Fault Property 1"
    },
    "Exception": {
        "ClassName": "System.ServiceModel.FaultException`1[App.dcFault]",
        "Message": "OH NO!",
        "Data": {},
        "InnerException": null,
        "HelpURL": null,
        "StackTraceString": "   at App.REST_HelloService.SayHelloFault()\r\n   at SyncInvokeSayHelloFault(Object , Object[] , Object[] )\r\n   at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)\r\n   at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc)\r\n   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc)\r\n   at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)",
        "RemoteStackTraceString": null,
        "RemoteStackIndex": 0,
        "ExceptionMethod": "8\nSayHelloFault\nApp_Svc, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\nApp.REST_HelloService\nVoid SayHelloFault()",
        "HResult": -2146233087,
        "Source": "App_Svc",
        "WatsonBuckets": null,
        "code": [
            {}
        ],
        "reason": [
            {}
        ],
        "messageFault": null,
        "action": "Custom Fault Action",
        "detail": {
            "sProp2": "Fault Property 2",
            "sProp1": "Fault Property 1"
        }
    },
    "Message": "OH NO!",
    "StackTrace": "   at App.REST_HelloService.SayHelloFault()\r\n   at SyncInvokeSayHelloFault(Object , Object[] , Object[] )\r\n   at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)\r\n   at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc)\r\n   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc)\r\n   at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)"
}

Folgende Properties stehen zur Verfügung, um am Client ggf. eine adequate Fehlermeldung anzeigen zu können:

Property Typ Beschreibung
Action String Beinhaltet die angegebene FaulAction
Code Object Beinhaltet den angegebenen FaulCode inkl. aller SubCodes
Reason Object Beinhaltet den angegebenen FaulReason, ggf. mit allen übergebenen Übersetzungen
Detail User Defined Beinhaltet das Objekt, welches vom Entwickler als Fault in die FaultException gegeben wurde. Im Beispiel die Variable fault vom Typ dcFault
Exception Object Beinhaltet die in Gänze serialisierte Exception mit allen Properties
Message String Beinhaltet das Message-Property der Exception
StackTrace String Beinhaltet das StackTraceString-Property der Exception

JSON Format

JSON kann verschiedene Datentypen in unterschiedlichsten Formaten übertragen. Für eine Datums- und Zeitangabe gibt es im JSON z.B. keinerlei Spezifikation, wie diese Information dargestellt werden soll. Da es im JSON keinen dedizierten DateTime-Typ gibt, wird ein Datum meist als String repräsentiert. Wie genau dieser String letztenendes aussieht, hängt vom JSON-Serializer ab.

Der in WCF integrierte DataContractJsonSerializer von Microsoft überträgt den .NET-Typ DateTime z.B. so:

{
    "datetime": "/Date(1335205592410-0500)/"
}

JavaScript serialisiert ein Datum aber so:

{
    "datetime": "2012-04-23T18:25:43.511Z"
}

Der Effekt ist, dass sich ggf. Server und Client nicht verstehen und letztenendes auf Fehler laufen, da sie einen unterschiedlichen "JSON-Dialekt" sprechen. Aus diesem Grund arbeitet der REST Endpoint in einem Framework Studio Service Host mit dem bekannten JSON.NET Serializer, der weltweit in millionen von Anwendungen zum Einsatz kommt. Das von diesem Serializer ausgegebene JSON ist kompatibel mit JavaScript und versteht im Umkehrschluss auch die meisten Dialekte, ohne dass der Entwickler selbst in den Serialisierungsprozess eingreifen muss.

ref- und out-Parameter

WCF unterstützt generell ref- und out-Parameter in Service Methoden. Dies mündet in der SOAP-Kommunikation letztenendes in mehreren Rückabewerten einer Methode. Der REST Endpoint unterstützt ref- und out-Parameter NICHT. Wenn mehr als ein Rückgabeparameter in einer Service-Methode nötig sind, wird empfohlen, einen DataContract für diesen Zweck zu erstellen und diesen als Rückgabetypen für die Methode zu verwenden (siehe Beispiel 3).

Individueller Rest-Endpoint

Mit der Checkbox Generate REST Endpoint wird ein Endpoint mit Standard-Einstellungen definiert. Werden bei den Operationen große Datenmengen transportiert oder dauern die Operationen ungewöhnlich lange, dann kann es zu entsprechenden Fehlermeldungen oder Timeouts kommen.

Es kann manuell ein REST-Endpoint mit individuellen Binding-Einstellungen definiert werden:

  1. Die Checkbox Generate REST Endpoint darf nicht mehr gesetzt sein.
  2. Es muss ein neuer EndPoint angelegt werden. Bei diesem muss als Name rest gesetzt sein. Dieser Name wird, wie beim automatischen REST-Endpoint, erkannt und so entsprechend für die JSON-Kommunikation konfiguriert.
  3. Wenn gewünscht, kann die Address angepasst werden. Die automatisch generierte Adresse bekommt am Ende den Namen des EndPoints - (.../rest). Damit das Verhalten identisch zum automatischen REST-Endpoint ist (.../api), muss die Address mit dem folgenden Wert überschrieben werden:
    http://%SERVER%:%PORT%/%HOSTNAME%/api
  4. Bei Binding muss webHttpBinding ausgewählt werden.
  5. Es kann eine eigene Binding Configuration angegeben werden. Diese muss zuvor im Register EndPoint Bindings erstellt werden (siehe unten).
  6. Behavior darf nicht angegeben werden, darum kümmert sich der Automatismus.

rest-manual-endpoint

Die Binding Configurationen kann folgendermaßen definiert werden:

  1. Ein neues EndPoint Binding anlegen und den Namen vergeben (z.B. RestEndPointBinding)
  2. Bei Binding Type muss webHttpBinding definiert werden.
  3. In den Registerkarten Default Properties und Reader Quotas Properties können die Größen entsprechend angepasst werden.
  4. Bei Bedarf können auch die verschiedenen Timeout-Zeiten erhöht werden.
Warning

Im folgenden Screenshot wurde überall der maximale Wert 2147483647 angegeben - das sind 2 GB. Das ist bequem und man hat keine Sorge mehr, dass Nachrichten die Konfiguration sprengen. Das bedeutet aber, dass der Service praktisch keinen Schutz mehr vor zu großen Nachrichten hat. Ein Angreifer könnte diesen Umstand nutzen und dem Service schaden.

Dasselbe gilt auch bei sehr großzügigen Timeout-Zeiten.

rest-manual-endpoint-binding