Magazine

WCF, REST, XML, JSON-P et accessoirement jQuery

Publié le 10 mars 2010 par Olivier Duval

Préambule



Imaginons que l'on veuille développer un service web d'interrogation basé sur REST, c'est à dire qui s'appuit sur HTTP uniquement pour les requêtes et les (codes : 200, 400, 404, 403, ...) réponses. Les réponses peuvent être au format Xml ou JSON. Ce service reste simple : une entrée du type int, et un retour d'un type primitif : string, ou un tableau de string : pour un n° de responsable, avoir les n° associations qu'il gère.

côté client : JSON et JSON-P




    

Le retour au format JSON est souvent pris pour un usage côté client / navigateur, par exemple à des fins de création de widgets à l'aide d'APIs, plus particuliérement pour un traitement javascript (facilité par ce format natif javascript) dans un mode Ajax. Comme chacun le sait ou a été déjà confronté, les appels Ajax ne peuvent être cross-domain, c'est à dire qu'un appel d'un script hébergé et à partir du domaine dummy.com vers domain.ntld est interdit par le navigateur (par l'objet XMLHttpRequest).



Les méthodes les plus connues pour palier à cette restriction sont :


  • soit développer un proxy côté serveur dummy.com qui effectura ensuite la requête vers le domaine distant domain.ntld pour renvoyer ensuite le résultat au script appelant,
  • soit se baser sur une fonction de callback JSON-P (JSON with Padding), technique qu'offre bon nombre d'APIs de nos réseaux sociaux préférés (Flickr, Delicious, ...) : il s'agit de mettre en place une URL dans une balise <script> créée dynamiquement, où le contenu de retour au format JSON sera encapsulé dans une fonction javascript (le callback), exécutée lors du retour de l'appel.

le résultat ressemblera à quelque chose du type :

<script type="text/javascript" script="http://domain.ntld/api/resp/2?callback=myfct">
  myfct({dataJSON});
</script>

par exemple, pour un appel Flickr, sur un bouton, la balise <script> avec l'URL d'interrogation sera créée dynamiquement, la fonction callbackFctFlickr avec les données au format JSON sera appelée au retour de l'appel :

$j('#btnSearch').click(function() {
            createScriptJSONP();
        });
 
        function createScriptJSONP() {
        $j('#results').empty();
        var urljsonp = $j('#sApi :selected').val();
        var api = $j('#sApi :selected').text();
 
        var s = document.createElement('script');
        if (api == "Delicious") {
            s.src = urljsonp + $j('#txtSearch').val() + '?callback=callbackFctDelicious';
        } else {
            s.src = urljsonp + $j('#txtSearch').val();
        }
        s.type = 'text/javascript';
        document.body.appendChild(s);
    } // création manuelle du script pour l'appel API
 
    function callbackFctFlickr(data) {                
        $j('#results').append('<ul>');
        $j(data.items).each(function(i, v) {            
            $j('#results').append("<li id=" + i + "><a href='" + v.link + "'>" + v.title + "</a><br/></li>");
            $j("<img/>").attr("src", v.media.m).appendTo("#" + i);            
        });
        $j('#results').append('</ul>');
    }

Amusons-nous avec 3 APIs : Flickr, Yahoo, Delicious , à tester sur la page démo en sélectionnant l'API souhaitée (essayer amélia ou architecture pour Flickr, photography pour Delicious, les deux pointent vers mes comptes),

Une alternative avec jQuery, où la création dynamique du code <script> s'effectue automatiquement, ainsi que l'ajout de la fonction callback, grâce à la fonction getJSON() ou ajax(), cette dernière offre plus d'options, et donc, de souplesse.

$j('#btnSearch').click(function() {
            createScriptJSONP();
        });
 
   function createScriptJSONP() {
        $j('#results').empty();
        var urljsonp = $j('#sApi :selected').val();
        var api = $j('#sApi :selected').text();
        var url = urljsonp + $j('#txtSearch').val();
 
        $j.ajax({
            url: url,
            dataType: 'jsonp',
            jsonp: api=='Flickr'?'jscallback': 'callback',
            success: function(data) {
                switch (api) {
                    case 'Flickr':
                        callbackFctFlickr(data);
                        break;
                    case 'Yahoo':
                        callbackFctYahoo(data);
                        break;
                    case 'Delicious':
                        callbackFctDelicious(data);
                        break;
                }
            }
        });

exemples d'appels générés sur Flickr, Yahoo ou Delicious avec jQuery, la fonction précisée en paramètre à jsoncallback ou callback est ajoutée par jQuery

http://api.flickr.com/services/feeds/photos_public.gne?id=55936300@N00&tagmode=any&format=json&tags=am%C3%A9lia&jsoncallback=jsonp1268130184172&_=1268130185391

http://search.yahooapis.com/ImageSearchService/V1/imageSearch?appid=YahooDemo&output=json&query=am%C3%A9lia&callback=jsonp1268130547073&_=1268130655088

http://feeds.delicious.com/v2/json/zorky/photography?callback=jsonp1268130547074&_=1268130676051

Bien entendu, cette technique fonctionne si le serveur distant encapsulte le JSON (qui doit être bien formé) dans une fonction javascript, à la JSON-P.

côté serveur

En .NET, WCF permet nativement de renvoyer au choix, des données au format XML (POX, Plain Old XML, par défaut) ou au format JSON, voire en binaire, pour des images par exemple, mais point d'encapsulation JSON-P (qu'on se rassure, cela arrive avec la version WCF de .NET 4.0, avec le binding webHttpBindingWithJsonP).

En attendant, en cherchant sur Goooooogle, on tombe facilement sur JSON with Padding (AJAX) qui permet d'étendre WCF pour ajouter une fonction callback à un appel JSON qui le demande.

Au lieu de recevoir les données au format JSON, par exemple

{"mylistJSONResult":["56056","56080","56129","56134","56227","56257"]}

on aura les données JSON encapsulées dans une fonction, avec un appel du type ?callback=myfct :

myfct( {"mylistJSONResult":["56056","56080","56129","56134","56227","56257"]} );

afin d'utiliser ce hook, dans le contrat WCF, on aura un attribut JSONPBehaviour qui nous permettra de préciser la fonction de callback :

[OperationContract]
        [WebCache(CacheProfileName = "cachingrest")]
        [WebGet(UriTemplate = "assoc/resp/{respid}.json",
            ResponseFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Wrapped)]
        [JSONPBehavior(callback = "callback")]
        List<string> mylistJSON(string respid);

l'attribut [WebCache] est tiré du WCF Rest Starter Kit qui permet de cacher (entêtes cache-control / expires) les requêtes GET de ce type. Rappelons que c'est l'un des grands avantages des requêtes GET par rapport aux POST : la capacité d'utiliser le cache navigateur.

l'attribut [WebGet] précise le motif de la requête REST, en GET, ainsi que le format de données retourné, ici JSON en mode wrappé (c'est à dire de la forme "mylistJSONResult" : [data]): on aura une URI du type http://domain.tld/mysvc.svc/json/assoc/resp/45.json?callback=myfct

Malheureusement, le code JSONPBehavior fonctionnera sur tout le service, que cela soit pour du JSON ou du ...XML.

Comme on veut servir du JSON-P ET du XML, on devra avoir 2 méthodes dans le contrat (et dans son implémentation donc), et un discriminant dans la configuration Web.config, pour déclencher le hook sur l'appel opportun. Pour bien faire, il s'agirait de modifier le module afin qu'il ne traite pas (ie : ajout de l'encapsulation JSON dans une fonction) les données lorsqu'il s'agit d'un format XML, je n'ai pas emprunté ce chemin.

le contrat de nos 2 méthodes, 1 pour le XML, 1 pour le JSON-P, qui répondent selon le motif URL :

/// retour XML
                /// URL au format http://localhost/mysvc.svc/xml/assoc/resp/{id}
        [OperationContract]
        [WebCache(CacheProfileName = "cachingrest")]
        [WebGet(UriTemplate = "assoc/resp/{respid}")]
        List<string> mylist(string respid);
 
                /// retour JSON encapsulé
                /// URL au format JSON pur http://localhost/mysvc.svc/json/assoc/resp/{id}.json
                /// ou JSON-P http://localhost/mysvc.svc/json/assoc/resp/{id}.json?callback=myfct
        [OperationContract]
        [WebCache(CacheProfileName = "cachingrest")]
        [WebGet(UriTemplate = "assoc/resp/{respid}.json",
            ResponseFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Wrapped)]
        [JSONPBehavior(callback = "callback")]
        List<string> mylistJSON(string respid);

et le web.config, où le descrimant s'effectue grace à l'attribut address :

<system.serviceModel>  
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />    
    <services>
     <service name="ws.rest.Assoc">
      <!-- cas JSON : extension JSONP -->
        <endpoint address="json"
          behaviorConfiguration="ws.rest.AssocRest"
          binding="customBinding"
          bindingConfiguration="jsonpBinding"
          contract="ws.rest.IAssoc" />
      <!-- cas XML -->
        <endpoint address="xml"
          behaviorConfiguration="ws.rest.AssocRest"
          binding="webHttpBinding"
          contract="ws.rest.IAssoc" />
      </service>
    </services>    
   <behaviors>   
    <endpointBehaviors>
          <behavior name="ws.rest.AssocRest">
             <webHttp />
          </behavior>
    </endpointBehaviors>
   </behaviors>
   <bindings>      
      <customBinding>        
        <binding name="jsonpBinding">
          <jsonpMessageEncoding/>
          <httpTransport manualAddressing="true"/>
        </binding>
      </customBinding>
   </bindings>
   <extensions>
      <bindingElementExtensions>
        <add name="jsonpMessageEncoding"
          type="ws.rest.JsonpBindingExtension, ws"/>
      </bindingElementExtensions>
   </extensions>
</system.serviceModel>

pas forcément la solution la plus élégante, nous sommes bien d'accord, .NET 4 apportera très certainement une réponse plus propre à ce type de problématique.

La prochaine fois, nous verrons la truite saumonée en papillote.


Retour à La Une de Logo Paperblog

A propos de l’auteur


Olivier Duval 4 partages Voir son profil
Voir son blog

l'auteur n'a pas encore renseigné son compte l'auteur n'a pas encore renseigné son compte

Dossier Paperblog