Klient REST w WCF

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, której to dostarczamy interface mapujący restowe operacje na metody. Jest to bardziej skonkretyzowana klasa względem ChannelFactory, nastawiona na czysto webową komunikację. WebChannelFactory potrafi także mapować odpowiedź serwera na odpowiednie klasy – obsługiwany jest zarówno XML jak i JSON. Myślę, że właśnie od owego mapowania warto zacząć pracę.

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:

Dzięki!

5 Responses to “Klient REST w WCF”


  1. 1 Viilot

    Tekst w sam raz do playbacku!

  2. 2 johny-max

    A ja sie nie zgodze…

  3. 3 fotka

    niezły wpis. pozdrawiam

  4. 4 best case for droid x

    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.

  5. 5 kredyt gotówkowy

    Thanks for sharing! I think I could read it all day:)

Leave a Reply