Jak już się napisało, że kiedyś opiszę się tworzenie klienta RESTa w .Necie, to słowa trzeba dotrzymać. Postaram się jak najzwięźlej opisać w jak dość prosty sposób można otrzymać taką funkcjonalność. Przykładem będzie właśnie API dla Blipa, bowiem to przy nim “wypłynęło” kilka ciekawych zagadnień.
Na wstępie dla niewtajemniczonych:
Całość opiera się na klasie WebChannelFactory
Blip na zapytanie dot. statusu odpowiada takim kodem:
{
'id': 1,
'created_at': "2007-10-18 11:27:20",
'transport': {'id': 6, 'name': 'www'},
'body': 'foobar http://rdir.pl/jk2hg',
'type': 'Status',
'user_path': '/users/frania',
'pictures_path': '/updates/1/pictures',
'recording_path': '/updates/1/recording',
'movie_path': '/updates/12/movie'
}
Klasa w C# może wyglądać tak:
[DataContract]
public class Update
{
[DataMember(Name="id")]
public int Id { get; set; }
[DataMember(Name="created_at")]
public string CreatedAt { get; set; }
[DataMember(Name="transport")]
public Transport Transport { get; set; }
[DataMember(Name="body")]
public string Body { get; set; }
private UpdateTypes type;
[DataMember(Name = "type")]
public string Type { get; set; }
[DataMember(Name="user_path")]
public string UserPath { get; set; }
[DataMember(Name="recipient_path")]
public string RecipientPath { get; set; }
[DataMember(Name="pictures_path")]
public string PicturesPath { get; set; }
[DataMember(Name="recording_path")]
public string RecordingPath { get; set; }
[DataMember(Name="movie_path")]
public string MoviePath { get; set; }
}
Jak widać do wskazania klasy na którą odpowiedź ma być deserializowana używa się atrybuty DataContract, natomiast dla wartości – DataMember z opcjonalną właściwością Name. Według opisu API wartość type może mieć przyjmować tylko 4 wartości (Status, DirectMessage, PrivateMessage, Notice). Naturalne wydaje się być użycie enumeratora. Niestety, deserializer JSONa nie potrafi parsować ciągów znaków na enum – dozwolone są tylko wartości liczbowe. Podobnie jest z datą – nie można w tym przykładzie dać typu DateTime właściwości CreatedAt, ponieważ odpowiedź ze strony serwera ma postać np. 2007-10-18 11:27:20, natomiast .Net wymaga Date([ilość ticków]). Problem można rozwiązać poprzez dopisanie właściwości/metod samodzielnie parsujących te dane.
Następnym krokiem jest utworzenie interface’u, który stanowić będzie “kontrakt” między klientem a serwerem. Tak jak dla klas danych, również i tutaj trzeba nadać odpowiedni atrybut – ServiceContract.
[ServiceContract]
public interface IBlipApi
{
}
Do każdej z metod również musimy dodać atrybuty. Zawsze musi się pojawić OperationContract oraz drugi atrybut określający sposób komunikacji
public interface IBlipApi
{
[OperationContract]
[WebGet(UriTemplate = "/updates/{id}", ResponseFormat = WebMessageFormat.Json)]
Update GetUpdate(string id);
[OperationContract]
[WebInvoke(UriTemplate = "/updates?update[body]={body}", Method = "POST"]
void CreateUpdate(string body);
[OperationContract]
[WebInvoke(UriTemplate = "/updates/{id}", Method="DELETE")]
void RemoveUpdate(string id);
}
WebGet jest de facto atrybutem WebInvoke z ustawioną metodą GET. W parametrze UriTemplate podajemy ścieżkę (bez adresu serwera – o tym później) zapytania. W nawiasach klamrowych pojawią się wartości z parametrów przekazanych do metody. Niestety, owe parametry mogą być tylko typu string (nie wiem czemu przy innych typach nie może być wywoływana metoda ToString()). W metodach zwracających dane trzeba jeszcze określić jakiego są one typu – domyślnie XML.
Tak gotowy interface trzeba przekazać fabryce.
WebChannelFactory<IBlipApi> channelFactory = new WebChannelFactory<IBlipApi>(new WebHttpBinding(), new Uri("http://api.blip.pl"));
IBlipApi api = channelFactory.CreateChannel();
W teorii byłoby to na tyle. Ale jest ‘ale’ ;-) API Blipa (tak jak pewnie wiele innych) potrzebuje w nagłówku żądania dwóch parametrów: Accept: application/json oraz X-Blip-API: 0.02. Nie znalazłem innego rozwiązania jak skorzystać z kontekstu i ustawienia w nim odpowiednich nagłowków
WebChannelFactory<IBlipApi> channelFactory = new WebChannelFactory<IBlipApi>(new WebHttpBinding(), new Uri("http://api.blip.pl"));
IBlipApi api = channelFactory.CreateChannel();
using(OperationContextScope context = new OperationContextScope((IContextChannel)api))
{
WebOperationContext.Current.OutgoingRequest.Accept = "application/json";
WebOperationContext.Current.OutgoingRequest.Headers.Add("X-Blip-API", "0.02");
WebOperationContext.Current.OutgoingRequest.Headers.Add("Authorization", string.Format("Basic {0}",
EncodeTo64(string.Format("{0}:{1}", user, password))));
Console.WriteLine(api.GetUpdate("555").Body);
}
W powyższym przykładzie dodałem jeszcze w nagłówku dane do autoryzacji w serwisie. Drugim “ale” jest odpowiedź z serwera Blip – .Net spodziewa się otrzymać Content-Type: application/json, tymczasem dostaje text/js przez co wyrzuca wyjątkiem. Najlepiej byłoby móc ustawić, iż zawsze z tego serwera otrzymujemy dane w postaci JSONa. W tym celu musimy utworzyć własny obiekt Binding() i przekazać mu inny obiekt z metodą informującą o tym, że zwrócone przez serwer dane to obsługiwany format. Brzmi zawile ale kod powinien to wyjaśnić. Najpierw tworzymy klasę dziedziczącą po WebContentTypeMapper.
internal class BlipMapper : WebContentTypeMapper
{
public override WebContentFormat GetMessageFormatForContentType(string contentType)
{
return WebContentFormat.Json;
}
}
Czego byśmy nie dostali w odpowiedzi traktujemy to zawsze jako JSON. Oczywiście, bardziej ambitni mogą zacząć tutaj odpowiednio sprawdzać przychodzącą wartość. Następnie definiujemy taką metodę:
private Binding GetBinding()
{
CustomBinding binding = new CustomBinding(new WebHttpBinding());
WebMessageEncodingBindingElement element = binding.Elements.Find<WebMessageEncodingBindingElement>();
element.ContentTypeMapper = new BlipMapper();
return binding;
}
Tworzy ona nasz własny obiekt Binding() i dodaje do niego wcześniej zdefiniowanego mappera. Trzeba jeszcze tylko zmodyfikować jedną linię:
WebChannelFactory<IBlipApi> channelFactory = new WebChannelFactory<IBlipApi>(GetBinding(), new Uri("http://api.blip.pl"));
Voila!
Jak widać utworzenie klienta REST w .Necie 3.5 nie jest trudne – większość operacji przejmuje na siebie WCF. Jednak nie ma róży bez kolców – nie wszystkie typy danych łatwo się mapują, przekazywane w parametrach wartości muszą być stringami i nie ma żadnego mechanizmu ich sprawdzania. Także zabawa z kontekstami przy ich dużej ilości może być kłopotliwa.
Ten wpis nie powstał “znikąd”. Pomogły następujące strony:
- Carlo’s blog – kilka przykładów użycia WCF
- dyskusja nt. dostępu do nagłówków HTTP
- użycie atrybutu WebInvoke
- kurs REST i WCF (linki do poprzednich części w treści wpisu)
- dyskusja WCF i JSON
Dzięki!


Tekst w sam raz do playbacku!
A ja sie nie zgodze…
niezły wpis. pozdrawiam
Reading this reminds me of my previous room mate. That guy was one of the smartest human beings I know, but he was a little weird for my tastes though. Anyways I delighted in reading this, thanks. Will give me something to discuss when I see him.
Thanks for sharing! I think I could read it all day:)