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.
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.
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:
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.
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.
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.
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"
}
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:
Als Rückgabetyp dient der DataContract dcHelloResult
mit drei Properties, welcher so definiert ist:
Der Aufruf der Service-Methode gestaltet sich intuitiv wie folgt:
Hat eine Methode mehrere komplexe Parameter, so gilt dieselbe Nomenklatur wie in Beispiel 2.
public virtual dcHelloResult SayHelloMultiple(dcHellopInput input, dcHelloInput inputOther, string greeting)
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).