Sie sind hier: Weblog

WCF WebAPI: Erste Schritte

Foto ,
12.12.2011 01:00:00

Unterstützung für REST gibt es in der WCF schon längere Zeit, wobei diese bis dato eher eingeschränkt war. Dies ist mitunter der Tatsache geschuldet, dass WCF das zum Einsatz kommende Transportprotokoll „weg-abstrahiert“. Gerade bei REST benötigt man jedoch die volle Kontrolle über die HTTP-basierten Nachrichten. Die in den Startlöchern stehende WCF WebAPI [1] soll hier Abhilfe schaffen. Dieser Blog-Eintrag zeigt, wie man eine einfache Hotel-Reservierung mit dieser neuen Bibliothek entwickeln kann.

Einrichten

Am einfachsten kommt man via NuGet [3] in den Genuss der aktuellen Vorab-Version von WebAPI. Es handelt sich dabei um einen Package-Manager, welcher zur Verfügung gestellte Komponenten aus einem Repository herunterlädt und direkt ins gewünschte Visual Studio-Projekt integriert. NuGet kann über den Extension Manager von Visual Studio 2010 heruntergeladen und eingerichtet werden (Tools | Extension-Manager). Danach kann NuGet über den Befehl Manage NuGet Packages, welcher sich im Kontextmenü der einzelnen Projekte im Solution-Explorer wieder findet, zum Herunterladen der WebAPI herangezogen werden. Dazu wird im NuGet-Dialog einfach nach WCF WebAPI gesucht und die Komponente WebApi.all ausgewählt. Das hier besprochene Beispiel geht davon aus, dass die Preview 6 von WebAPI im Rahmen einer Web-Anwendung Verwendung findet.

Datenvertrag und Services

Zunächst werden Datenverträge eingerichtet. Im betrachteten Beispiel handelt es sich hierbei um eine Klasse Hotel sowie um eine weitere Kasse HotelBuchung.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Serialization;

namespace WebAPI
{
    [DataContract(Namespace="www.softwarearchitekt.at/services/hotel")]
    public class Hotel
    {
        public Hotel() { 
            LastModified = DateTime.Now;  
        }

        [DataMember]
        public int HotelId { get; set; }

        [DataMember]
        public string Name { get; set; }

        [DataMember]
        public int Sterne { get; set; }

        [DataMember]
        public DateTime LastModified { get; set; }

        public override string ToString()
        {
            return HotelId + " " + Name + " " + Sterne;
        }


    }

    [DataContract(Namespace = "www.softwarearchitekt.at/services/hotel")]
    public class HotelBuchung {

        [DataMember]
        public int HotelBuchungId { get; set; }

        [DataMember]
        public int HotelId { get; set; }

        [DataMember]
        public string Vorname { get; set; }

        [DataMember]
        public string Nachname { get; set; }
        
    }

}

Anschließend wird eine Klasse, welche den Service repräsentiert, eingerichtet. Zur Vereinfachung verwaltet sie Hotels und Buchungen in jeweils einer statischen Liste. Die weiteren Methoden dieser Klasse, welche die angebotenen Service-Operationen darstellen, werden nachfolgend beschrieben.

 

public class HotelService
{
    private static List<Hotel> hotels;
    private static List<HotelBuchung> buchungen;

    static HotelService()
    {
        hotels = new List<Hotel>();
        hotels.Add(new Hotel { HotelId = 1, Name = "Hotel zur Post", Sterne = 2 });
        hotels.Add(new Hotel { HotelId = 2, Name = "Burj Al Arab", Sterne = 6 });
        hotels.Add(new Hotel { HotelId = 3, Name = "Ibis", Sterne = 4 });

        buchungen = new List<HotelBuchung>();
        buchungen.Add(new HotelBuchung { HotelBuchungId = 1, HotelId = 1, Vorname = "Max", Nachname = "Muster" });
        buchungen.Add(new HotelBuchung { HotelBuchungId = 2, HotelId = 1, Vorname = "Klara", Nachname = "Fall" });
        buchungen.Add(new HotelBuchung { HotelBuchungId = 3, HotelId = 2, Vorname = "Marie", Nachname = "Juana" });
        buchungen.Add(new HotelBuchung { HotelBuchungId = 4, HotelId = 2, Vorname = "Frank", Nachname = "Reich" });
    }

    […]

}

Operationen zum Abfragen von Daten

Um eine Service-Operation, welche Hotels nach ihren Sternen abfragt, bereitzustellen, wird eine entsprechende Methode eingerichtet. Zusätzlich ist sie mit dem Attribut WebGet zu versehen. Dieses Attribut legt fest, dass die Methode über das HTTP-Verb GET erreichbar sein soll. Das UriTemplate definiert die URL, über welche die Operation zur Ausführung gebracht werden kann. Platzhalter, welche auf gleichnamige Übergabeparameter abgebildet werden, sind dabei in geschweiften Klammern zu halten. Dabei fällt positiv auf, dass – im Gegensatz zur aktuellen Version 4 von WCF – auch Datentypen jenseits von String unterstützt werden. Im betrachteten Fall wird zum Beispiel ein int mit dem Namen minSterne übergeben.

[WebGet(UriTemplate = "?minSterne={minSterne}")]
public List<Hotel> GetHotelsBySterne(int minSterne)
{
    return hotels
            .Where(h => h.Sterne >= minSterne)
            .ToList();
}

ODATA-Unterstützung

Die im letzten Abschnitt besprochene Methode erlaubt lediglich das Einschränken der abzurufenden Hotels anhand des Attributs Sterne. Wem das zu wenig ist, kann auf die in WCF WebAPI integrierte ODATA-Unterstützung zurückgreifen. Bei ODATA [2] handelt es sich um einen Standard, der es unter anderem erlaubt, Abfragen via HTTP zu formulieren. Um in den Genuss dieses Standards zu kommen, ist lediglich eine REST-Operation, welche als Rückgabewert IQueryable<T> liefert, einzurichten. 

[WebGet(UriTemplate="")]
public IQueryable<Hotel> GetHotels()
{
    return hotels.AsQueryable();
}

Das schöne dabei ist, dass IQueryable<T> auch von O/R-Mappern wie Entity Framework verwendet wird. Somit können vordefinierte Abfragen beim Aufruf einer Operation um weitere Einschränkungen ergänzt werden bevor sie in der Datenbank ausgeführt werden.

?

POST, PUT, DELETE und Co.

Methoden, welche mit HTTP-Verben jenseits von GET angesprochen werden sollen, sind mit WebInvoke zu annotieren. Dabei wird neben dem UriTemplate das gewünschte Verb im Attribut Method als String übergeben.?

?Die hier gezeigte Methode zeigt auch einen Nachteil des aktuellen Programmiermodells für REST-basierte Services in WCF: Eigentlich müsste diese Methode den Status-Code „201 CREATED“ zurückliefern. Dies zu bewerkstelligen ist in WCF 4 aufgrund der Tatsache, dass das Transportprotokoll (aus der Sicht von WCF ist dieses hier HTTP) abstrahiert nicht, jedoch nicht ganz einfach. Die WCF WebAPI bietet eine elegante Lösung zu diesem Problem. Diese wird am Ende des Artikels besprochen.

 

Services registrieren

Bei der Verwendung von Web-Anwendungen bietet sich das Einrichten einer Route, welche auf den Service verweist, an. Dies wird in der global.asax, wie nachfolgend demonstriert, unter Verwendung der Klasse ServiceRoute bewerkstelligt. Das erste Konstruktor-Argument beinhalte die Basis-URL des Services, der zweite eine HttpServiceHostFactory und der dritte den Typ des Services. Die HttpServiceHostFactory kann auch analog zu klassischen WCF-Szenarien zur Kreierung einer ServiceHost-Instanz herangezogen werden. Der Parameter EnableHelpPage bewirkt, dass eine Hilfe-Seite, welche die vom Service erwarteten sowie generierten Nachrichten beschreibt, generiert wird. EnableTestClient legt hingegen fest, dass ein HTTP-basierter Client zum Testen der Services eingerichtet wird.

void Application_Start(object sender, EventArgs e)
{
    var factory = new HttpServiceHostFactory();
    factory.Configuration.EnableHelpPage = true;
    factory.Configuration.EnableTestClient = true;
            
    RouteTable.Routes.Add(new ServiceRoute("hotels", factory, typeof(HotelService)));
}

 

Wird das Projekt nun gestartet, können über die definierten URLs Daten direkt über den Browser der Wahl abgerufen werden. Die nachfolgende Abbildung zeigt den Aufruf der zuvor beschriebenen ODATA-basierten Operation.

 

Der Test-Client kann durch Anhängen von /test an die Basis-URL des Services erreicht werden. Das Schöne an diesem Client ist, dass er im Unterschied zu Alternativen, wie Fiddler, die einzelnen Services kennt, was zur Folge hat, dass er die vorhandenen Möglichkeiten inkl. Code-Vervollständigung anbieten kann, während er die nicht implementierten Optionen ausgraut.

?Durch Anhängen von /help an die Basis-Adresse der Service-Url wird die aus WCF 4 bzw. aus dem WCF Starter-Kit bekannte Hilfe-Seite angezeigt.  

Mit HTTP auf Du und Du

Möchte man die via HTTP übersendeten Nachrichten direkt beeinflussen, zum Beispiel um den Status Code einer Operation zu ändern oder um bestimmte Header-Einträge zu setzen bzw. auszulesen, können an der Stelle der eigentlichen Parameter-Typen die Typen HttpRequestMessage und HttpResponseMessage herangezogen werden. Wie die nachfolgenden Beispiele zeigen, existieren diese in einer typisierten und untypisierten Variante. Sie bieten über die Eigenschaft Headers Zugriff auf die Kopfzeilen und über Content Zugriff auf die Nutzdaten. Letztere können in verschiedenen Arten gelesen werden, zum Beispiel als String, als Stream oder als Objekt.

[WebInvoke(UriTemplate = "{hotelId}/buchungen", Method="POST")]
public HttpResponseMessage PostBuchungen(int hotelId, HttpRequestMessage<HotelBuchung> request)
{
    HotelBuchung buchung;
    string message;

    buchung = request.Content.ReadAsAsync().Result;
    buchung.HotelId = hotelId;
    buchung.HotelBuchungId = buchungen.Max(b => b.HotelBuchungId) + 1;
    buchungen.Add(buchung);

    var lang = request.Headers.AcceptLanguage.FirstOrDefault();
    message = GetReplyString(lang);

    var response = new HttpResponseMessage();
    response.StatusCode = HttpStatusCode.Created; // 201 Created
    response.Content = new StringContent(message);

            

    return response;
}


[WebInvoke(UriTemplate = "{hotelId}/message", Method = "POST")]
public void PostMessage(HttpRequestMessage request)
{
    using (var stream = request.Content.ReadAsStreamAsync().Result)
    {
        var reader = new StreamReader(stream);
        var contents = reader.ReadToEnd();
        File.AppendAllText(@"c:\temp\messages", contents + "\r\n");
    }
}

[WebGet(UriTemplate="{hotelId}")]
public HttpResponseMessage<Hotel> GetHotel(int hotelId)
{
    var hotel = hotels.Where(h => h.HotelId == hotelId).FirstOrDefault();

    if (hotel == null)
    {
        return new HttpResponseMessage<Hotel>(HttpStatusCode.NotFound);
    }

    var response = new HttpResponseMessage<Hotel>(hotel);
    response.Headers.ETag = new EntityTagHeaderValue("\"" + hotel.LastModified.ToString() + "\"");
    return response;
}

 

 Fazit

WCF WebAPI bietet direkten Zugriff auf HTTP und daneben Annehmlichkeiten wie ODATA-Unterstützung oder einen intelligenten Test-Client. Daneben werden endlich Datentypen jenseits von String von den UriTemplates unterstützt. An der einen oder anderen Stelle wirkt die API derzeit noch etwas Wortreich, sodass auf einige Convenience-Methoden in der finalen Version zu hoffen ist. Diese soll angeblich im ersten Quartal 2012 via NuGet veröffentlicht werden. Dies korreliert auch mit dem aktuellen sowie aus Sicht des Autors sehr lobenswerten Trend bei Microsoft, bei Möglichkeit, nicht auf die nächste .NET-Version zu warten, um Verbesserungen auszuliefern.