Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
FORUMS .NET FAQs .NET TUTORIELS .NET SOURCES .NET LIVRES .NET OUTILS .NET BLOG .NET DOTNET TV

Introduction aux services web REST avec WCF 3.5

WCF

Date de publication : 25/05/2008 , Date de mise à jour : 25/05/2008

Par Florian Casabianca (mon site)
 

Pas de note (Aucun commentaire)

Partons à la découverte des Web Services de type REST et de leur prise en charge dans WCF 3.5.

Introduction
I. Qu'est-ce que REST ?
II. Les nouveautés de WCF 3.5
II-A. La modélisation d'URI
II-B. Les attributs WebGet et WebInvoke
II-C. WebOperationContext
II-D. WebHTTPBinding
III. Création du service
III-A. Présentation de l'exemple
III-A-1. Les ressources et les URI
III-A-2. La représentation des ressources
III-A-3. Les méthodes
III-A-4. Les codes d'état
III-B. Le contrat
III-C. L'implémentation du contrat
III-C-1. GetIndex
III-C-2. GetFilm
III-C-3. GetFilms
III-C-4. CreateFilm
III-C-5. UpdateFilm
III-C-6. DeleteFilm
III-D. Test du service avec Fiddler
III-E. L'hébergement du service
IV. Client JavaScript
IV-A. L'objet XMLHttpRequest
IV-B. Récupérer un film
IV-C. Supprimer un film
IV-D. Ajouter un film
IV-E. Modifier un film
V. Client .NET
V-A. Récupérer un film
V-B. Récupérer la liste des films
V-C. Ajouter un film
V-D. Modifier un film
V-E. Supprimer un film
Conclusion
Liens
Sources
Remerciements
Contact


Introduction

Les Web services sont souvent associés à l'échange de messages SOAP via HTTP. La première version de WCF répondait parfaitement à ce type de scénario. Elle proposait de plus la possibilité d'utiliser un autre protocole de transport pour l'envoi des messages (TCP par exemple).

Mais une autre approche des services Web et qui devient de plus en plus populaire existe: REST. Une nouveauté? Pas vraiment. Le concept introduit en 2000 ne fait qu'utiliser les principes fondamentaux du Web. Utilisant le protocole HTTP, il permet l'envoi de messages sans enveloppe SOAP et dans un encodage libre (XML, JSON, binaire, simple texte). Il est actuellement très utilisé par les sites communautaires (ou réseaux sociaux) leur permettant de proposer à leurs clients une API facile à utiliser. Des sites comme Flickr, Facebook, Last.fm, Amazon proposent ainsi de telles API évitant à leurs clients de devoir passer par la case SOAP.

Avec la version 3.5 de WCF, un grand nombre de nouveautés ont été introduites et permettent de créer facilement des Web services REST.

Mais ne nous trompons pas, REST n'est pas le remplaçant des services Web SOAP. Il ne propose pas les caractéristiques des services Web SOAP avancés telles que la qualité de service, les transactions, les files d'attentes, etc. Il est en revanche très intéressant si l'on souhaite mettre à disposition une API facile à utiliser et plutôt orientée "données".

La première fait un tour d'horizon rapide des nouveautés de WCF 3.5 qui vont nous intéressés, la deuxième aborde la création du service tandis que la troisième est consacrée au développement de la partie cliente.

La lecture de cet article suppose que vous possédez déjà quelques notions sur WCF. Dans le cas contraire, de nombreuses ressources sur le sujet sont disponibles sur Internet (deux liens d'introduction à WCF sont disponibles en fin d'article.).


I. Qu'est-ce que REST ?

Representational state transfer (REST) n'est pas un protocole, un standard ou encore un format. Il s'agit d'un style d'architecture pour les systèmes distribués. Le terme a été introduit par Roy Fielding (un des principaux auteurs de la spécification HTTP) dans une thèse en 2000. Son implémentation la plus commune se trouve dans les documents hypertexte à travers le protocole HTTP, c'est-à-dire le web lui-même.

En effet, lorsque vous surfer sur le Web vous faites du REST sans le savoir. Prenons un exemple simple: vous souhaitez accéder aux informations concernant votre film préféré via un site spécialisé. Vous ouvrez donc votre navigateur et saisissez dans la barre d'adresse l'URL (par exemple HTTP://www.touslesfilms.com/monfilmpréféré) de la page dédiée à ce film (on suppose que vous la connaissez). Le navigateur va envoyer une requête HTTP GET à cette URI afin d'obtenir la ressource correspondante. Le serveur va analyser cette requête et renvoyer une réponse HTTP. Celle-ci sera composée d'un en-tête et d'un corps. Le corps contiendra du html (par exemple) qui s'affichera dans le navigateur. Comment ce dernier sait-il que la requête s'est bien déroulée (pas d'erreur dans l'adresse par exemple) et que les informations renvoyées sont au format html ? Grâce à l'en-tête de la réponse. Celui-ci est composé d'informations comme:

  • Un code de statut HTTP indiquant le déroulement de la requête. Un code de statut OK permet au navigateur de savoir que tout s'est déroulé correctement. Vous connaissez sans doute le code Not Found (Erreur 404) indiquant que la ressource demandée est introuvable.
  • Un type de contenu (Content-Type) indiquant le format des données du corps de la réponse HTTP. C'est ce qui permet au navigateur de savoir que le contenu est du html et de l'interpréter comme tel.
Toutes ces notions (et quelques autres) sont la base du style d'architecture REST.

Un des concepts important de REST est la notion de ressource. Chaque ressource est accessible par une URI (Uniform Resource Identifier). La ressource étant une notion abstraite, le client et le serveur communiquent en s'échangeant une représentation de la ressource. Le format de cette représentation peut être du XML, du JSON, une image, un fichier vidéo, etc.

Les ressources sont accessibles via un ensemble uniforme de commandes fourni par HTTP (essentiellement GET, POST, PUT et DELETE) qui permettent de spécifier l'opération à effectuer sur une ressource.

La communication entre le client et le serveur est "sans état". Ainsi chaque requête du client vers le serveur doit contenir toutes les informations nécessaires pour que cette demande soit comprise, et elle ne peut tirer profit d'aucun contexte stocké sur le serveur. L'état de la session est donc entièrement détenu par le client.

Vous trouverez plus d'information sur les principes de REST via les liens présents à la fin de cet article.


II. Les nouveautés de WCF 3.5

La version 3.5 du Framework .NET a apporté un grand nombre de nouveautés à WCF lui permettant entres autre une prise en charge explicite des communications de type REST. Voici donc un résumé de quelques-unes de ces nouveautés qui vont nous intéresser tout au long de cet article.


II-A. La modélisation d'URI

Les nouveaux types UriTemplate, UriTemplateTable et UriTemplateMatch de l'assembly System.ServiceModel.Web vont nous aider à manipuler les URI.

La classe UriTemplate permet de définir un modèle d'URI. Celui-ci est constitué d'une série de segments délimités par une barre oblique /. Un segment peut être une valeur littérale, une valeur variable (écrite entre accolades {}) ou un caractère générique (* par exemple).
 UriTemplate template = 
 new UriTemplate("{rubrique}/articles/{langage}?sujet={nomsujet}");
L'objet UriTemplate possède une méthode Match permettant de faire correspondre un URI à un UriTemplate et ainsi de retrouver les valeurs des variables contenu dans l'URI.

Voici un exemple:
Uri prefix = new Uri("HTTP://developpez.com");
Uri fullUri = new Uri("HTTP://developpez.com/dotnet/articles/csharp?sujet=WCF");

UriTemplateMatch results = template.Match(prefix, fullUri);

if (results != null)
{
    foreach (string variableName in results.BoundVariables.Keys)
    {
        Console.WriteLine("{0}: {1}", variableName, results.BoundVariables[variableName]);
    }
}
Et le résultat produit:

D:\dotNET\developpez.com\Articles\REST\console.png
Il est ainsi très facile d'extraire les valeurs de variables contenues dans une URI dont on a défini le modèle.

A noter que ces types ne se trouvent pas dans l'assembly System.ServiceModel (qui est l'assembly permettant d'utiliser WCF). Cela vous permet de les utiliser même dans un projet non WCF.


II-B. Les attributs WebGet et WebInvoke

Les deux nouveaux attributs WebGet et WebInvoke permettent d'associer une opération de service (d'un contrat WCF) à une requête HTTP (ainsi qu'à un verbe de transport tel que GET ou POST) et à un modèle d'URI (que nous venons de voir précédemment). Ils offrent de plus la possibilité de contrôler le format de sérialisation/dé-sérialisation des données envoyées lors de la requête ou de la réponse HTTP.

Alors que l'attribut WebGet associe une opération au verbe HTTP GET, l'attribut WebInvoke l'associe à un des autres verbes disponibles (POST, PUT, DELETE, etc.). On spécifie alors le verbe exact à associer en utilisant la propriété Method.

Voici un exemple utilisant l'attribut WebGet:
[OperationContract]
[WebGet(UriTemplate = "films/{filmId}", ResponseFormat =    WebMessageFormat.Xml, BodyStyle = WebMessageBodyStyle.Bare)]
Film GetFilm(string filmId);

Dans cet exemple, toutes les requêtes HTTP GET effectuées sur l'URI HTTP://blabla/films/1 (par exemple) seront automatiquement prisent en compte par l'opération de service GetFilm. La variable filmId (ici "1") de l'URI correspondra à la variable filmId qui sera passée à l'opération.

Il faut noter que les variables de l'opération de service qui seront récupérées via l'URI doivent être des chaines de caractères. C'est ensuite au développeur d'éventuellement les convertir en un autre type (en entier par exemple).

La propriété ResponseFormat spécifie la sérialisation utilisée lors de la réponse HTTP (on passe du d'une sérialisation XML à une sérialisation JSON simplement en changeant la valeur de cette propriété et en gardant le même code !). La propriété BodyStyle définit le style de corps des messages envoyés vers et depuis l'opération de service. Bare signifie que les demandes comme les réponses ne sont pas encapsulées. Dans cet exemple on ne reverra que le XML correspondant à la sérialisation de l'objet Film et aucune balise supplémentaire ne sera ajoutée par WCF.

Un autre exemple utilisant l'attribut WebInvoke:
[OperationContract]
[WebInvoke(Method = "DELETE", UriTemplate = "films/{filmId}")]
void DeleteFilm(string filmId);

Dans cet exemple, toutes les requêtes HTTP DELETE effectuées sur l'URI HTTP://blabla/films/1 (par exemple) seront automatiquement prisent en compte par l'opération de service DeleteFilm. La variable filmId (ici "1") de l'URI correspondra à la variable filmId qui sera passée à l'opération.


II-C. WebOperationContext

WebOperationContext est une classe permettant l'accès aux propriétés contextuelles des demandes et des réponses Web. Vous pouvez obtenir une instance de cette classe par la propriété static Current qui renverra le contexte courant. A partir de là vous avez accès à quatre propriétés IncomingRequest, IncomingResponse, OutgoingRequest et OutgoingResponse.

IncomingRequest renvoie un objet de type IncomingWebRequestContext fournissant l'accès au contexte de la requête Web entrante (le message envoyé au service par le client). Vous pouvez donc récupérer par exemple les valeurs des en-têtes HTTP, la méthode HTTP utilisée (GET, POST, etc.), etc.

OutgoingResponse renvoie un objet de type OutgoingWebResponseContext fournissant l'accès au contexte de la réponse Web sortante (le message envoyé au client par le service). Il est ainsi possible par exemple de spécifier le type de contenu (ContentType) de la réponse, les valeurs des en-têtes HTTP ou encore le code d'état de la réponse (par exemple 404 correspond à une ressource introuvable).

Voici comment spécifier le code d'état de la réponse sortante (ici le code 404):
WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HTTPStatusCode.NotFound;

C'est cette technique que nous allons utiliser par la suite pour spécifier si une requête s'est bien déroulée ou pas.


II-D. WebHTTPBinding

Web HTTP Binding est un nouveau binding pour WCF apparu avec le Framework .NET 3.5. Il permet d'envoyer des informations sur HTTP ou HTTPs sans créer d'enveloppe SOAP. C'est donc le choix idéal pour des communications REST.

Ce binding offre trois options pour représenter les messages échangés: un encodage XML, JSON (JavaScript Object Notation) et binaire (utile pour envoyer des images par exemple).

Deux nouveaux behaviors font aussi leur apparition:

  • web HTTP - associé avec WebHTTPBinding il permet d'activer le mode de programmation Web pour un service WCF. C'est celui que nous allons utiliser.
  • enableWebScript - simplifie l'utilisation de Web service avec ASP.NET AJAX

III. Création du service


III-A. Présentation de l'exemple

Place maintenant à la pratique. Nous allons illustrer les différents concepts vus précédemment par la création d'un service REST très simple. Celui-ci permettra la visualisation et la manipulation de données concernant les films ayant fait les meilleures figures au box-office français. Le service devra donc pouvoir afficher la liste des films disponibles ou seulement les informations concernant un film en particulier. Le service devra aussi permettre l'ajout et la suppression d'un film ainsi que la modification d'un film existant. De plus, nous offrirons la possibilité de visualiser une page d'aide en html résumant le fonctionnement du service (nous verrons l'intérêt de cette page un peu plus loin).


III-A-1. Les ressources et les URI

Les ressources du système sont:

  • la liste des films
  • un film en particulier
  • une page d'aide
Nous aurons donc trois types d'URI: /films (pour accéder à la liste des films), /films/idFilm (avec idFilm correspondant à l'identifiant du film que l'on souhaite manipuler) et /aide renvoyant la page d'aide.


III-A-2. La représentation des ressources

Il nous faut maintenant décider de la représentation des ressources précédemment citées. Plus concrètement il s'agit de savoir quel va être le format des données échangées entre le service et le client. Si un client veut modifier un film, sous quel format enverra-t-il les modifications ? Quel sera le format des données lorsque qu'un client désirera par exemple obtenir les informations concernant le film d'identifiant 1 (/films/1) ? Nous pourrions par exemple renvoyer une image (l'affiche du film), un extrait sonore, ou encore une vidéo. Dans notre cas les informations renvoyées se borneront à des éléments du type titre du film, nombre d'entrées, etc. Nous allons donc choisir une représentation XML pour nos ressources.

Voici tout d'abord la classe Film qui, comme son nom l'indique, permettra de représenter un film.
Classe Film
namespace WcfServiceRestBoxOffice.Data
{
    [DataContract(Namespace = "HTTP://schemas.developpez.com/BoxOfficeREST/2008/03", Name = "Film")]
    public class Film
    {
        [DataMember(Order = 1)]
        public int Id { get; set; }

        [DataMember(Order = 2)]
        public String Titre { get; set; }

        [DataMember(Order = 3)]
        public int Annee { get; set; } //année de sortie

        [DataMember(Order = 4)]
        public double Entrees { get; set; }  //nombre d'entrées en millions

        [DataMember(Order = 5)]
        public Uri Uri { get; set; }
    }
}
Un film sera caractérisé par un identifiant, un titre, une année de sortie, le nombre d'entrées réalisées ainsi que par une URI.

WCF offre plusieurs techniques de sérialisation. Dans notre exemple nous utiliserons la sérialisation par défaut de WCF (DataContractSerializer). Pour en savoir plus sur la sérialisation au sein de WCF, un lien à la fin de cet article vous conduira vers plus d'informations sur le sujet.

Le résultat produit par cette sérialisation sur un objet de type Film sera le suivant:
XML résultant de la sérialisation d'un film
<Film xmlns="HTTP://schemas.developpez.com/BoxOfficeREST/2008/03" xmlns:i="HTTP://www.w3.org/2001/XMLSchema-instance">
  <Id></Id>
  <Titre> </Titre>
  <Annee></Annee>
  <Entrees></Entrees>
  <Uri> </Uri>
</Film>
Concernant la liste de films, nous allons faire quelque chose d'un peu plus intéressant que simplement sérialiser l'ensemble des films disponibles (et ne retourner au client qu'un empilement de balises Film).

Supposons par exemple que l'on dispose d'une base de données contenant des informations sur des milliers de films. Vous comprenez dès lors que sérialiser tous ces films et les renvoyer en une seule fois au client en faisant la demande ne se révèle pas être un choix très judicieux. Surtout si ce client ne souhaite en fait que la liste des 10 premiers films du box-office.

Nous allons donc créer un système de pagination. Le client pourra donc spécifier le nombre de films à inclure dans la liste ainsi que le nombre de films qu'il souhaite "sauter". Une liste de films sera toujours ordonnée par rapport au nombre d'entrées réalisées.

Par exemple, pour obtenir le top dix du box-office il faudra indiquer:

  • nombre de films à inclure: 10
  • nombre de films à "sauter": 0
Pour obtenir les dix films suivant il faudra indiquer:

  • nombre de films à inclure: 10
  • nombre de films à "sauter": 10
A chaque requête de ce genre nous renverrons au client une structure XML contenant: le nombre total de films disponibles, le nombre de films inclus dans la liste renvoyée, le numéro à partir duquel commence la séquence (qui correspond au nombre de films sautés + 1) et bien sûr la liste des films résultants de cette requête.

Voici donc la classe ListeFilms qui permet de mettre tout cela en place:
Classe ListeFilms
namespace WcfServiceRestBoxOffice.Data
{
    /// Représente une séquence de films
    [DataContract(Namespace = "HTTP://schemas.developpez.com/BoxOfficeREST/2008/03", Name = "ListeFilms")]
    public class ListeFilms
    {
        /// Nombre total de films
        [DataMember(Order = 1)]
        public int TotalCount { get; set; }

        /// Numéro à partir duquel commence la séquence
        [DataMember(Order = 2)]
        public int Start { get; set; }
        
        /// Nombre de films dans la séquence
        [DataMember(Order = 3)]
        public int TakeCount { get; set; }

        private List<Film> films = null;

        [DataMember(Order = 4)]
        public List<Film> Films
        {
            get
            {
                if (films == null)
                {
                    films = new List<Film>();
                }
                return films;
            }
            set
            {
                films = value;
            }
        }
    }
}
Bien sûr il est tout à fait possible d'utiliser plusieurs représentations pour une même ressource. Libre ensuite au client de choisir la représentation qu'il préfère. Par exemple un client Ajax préfèrera des données sous format JSON plutôt que sous format XML.

Enfin, la page d'aide en html (filmsserviceindex.html) sera inclue en tant que ressource dans l'assembly du service WCF et sera donc accessible via l'instruction:
 Properties.Resources.filmsserviceindex 
Le contenu de cette page est tout à fait banal et ne mérite pas que l'on se penche dessus.


III-A-3. Les méthodes

Nous venons de définir les ressources et la façon d'y accéder (via une URI). Cependant l'accès à une ressource peut prendre plusieurs formes. Nous pouvons par exemple vouloir récupérer la représentation d'une ressource, la modifier, la supprimer ou en ajouter une nouvelle. Les méthodes (notamment GET, POST, PUT et DELETE) du protocole HTTP vont nous permettre de spécifier l'opération à effectuer sur une ressource.

Le tableau suivant indique pour chaque ressource la liste des méthodes applicables ainsi que la représentation utilisée.

Resource Méthode Description Représentation
Film GET Obtenir les données d'un film Film au format XML
Film PUT Modifier les données d'un film Film au format XML
Film DELETE Supprimer un film Aucune
Liste de film GET Obtenir la liste des films Liste de films au format XML
Liste de film POST Ajouter un film Film au format XML

La première ligne signifie qu'une requête HTTP GET sur une ressource film verra le service renvoyer une réponse dont le corps contiendra la représentation du film demandé au format XML. La deuxième indique qu'un client désirant modifier un film devra envoyer une requête HTTP PUT sur l'URI du film à modifier. Le corps de cette requête devra contenir la représentation de la nouvelle version du film au format XML. La suppression d'un film se fera via une requête HTTP DELETE sur l'URI du film à supprimer. Une requête de type HTTP GET sur l'URI de la ressource "liste des films" renverra une réponse contenant dans son corps la liste des films au format XML. Enfin, pour ajouter un nouveau film le client devra effectuer une requête HTTP POST sur l'URI de la ressource "liste des films" dont le corps contiendra les données du film au format XML.


III-A-4. Les codes d'état

En plus des données demandées, le service doit indiquer un code d'état HTTP indiquant l'état du traitement de la requête (réussite, ressource non trouvée, accès non autorisé, etc.).

Voici un tableau résumant les codes d'états renvoyés par le service en fonction des requêtes:

Resource Méthode Description Codes
Film GET Obtenir les données d'un film 200 (OK) 400 (Bad Request) 404 (Not Found)
Film PUT Modifier les données d'un film 200 (OK) 400 (Bad Request) 404 (Not Found)
Film DELETE Supprimer un film 200 (OK) 400 (Bad Request) 404 (Not Found)
Liste de film GET Obtenir la liste des films 200 (OK) 400 (Bad Request)
Liste de film POST Ajouter un film 201 (Created) 400 (Bad Request)

III-B. Le contrat

Au sein de Visual Studio 2008 nous allons partir d'un projet de type WCF Service Library afin de développer notre service WCF REST. Ce type de projet compilera le service au sein d'une dll que nous hébergerons plus tard dans une application Web.

Il nous faut ensuite ajouter une référence à l'assembly System.ServiceModel.Web afin de bénéficier des nouveautés de WCF 3.5:

D:\dotNET\developpez.com\Articles\REST\reference.png

Place maintenant à la création du contrat de notre service WCF REST. Comme vous pouvez le voir, il s'agit d'un contrat WCF tout à fait classique avec création d'une interface IBoxOfficeREST décorée de l'attribut ServiceContract et de méthodes elles-mêmes décorées par l'attribut OperationContract.
Contrat du service
namespace WcfServiceRestBoxOffice
{
    [ServiceContract(Namespace = "HTTP://www.developpez.com/BoxOfficeREST/2008/03"), Name = "BoxOfficeREST")]
    public interface IBoxOfficeREST
    {
        [OperationContract]
        [WebGet(UriTemplate = "aide")]
        Stream GetIndex();
              
        [OperationContract]
        [WebGet(UriTemplate = "films", ResponseFormat = WebMessageFormat.Xml, BodyStyle = WebMessageBodyStyle.Bare)]
        ListeFilms GetFilms();

        [OperationContract]
        [WebGet(UriTemplate = "films/{filmId}", ResponseFormat = WebMessageFormat.Xml, BodyStyle = WebMessageBodyStyle.Bare)]
        Film GetFilm(string filmId);




        [OperationContract]
        [WebInvoke(Method = "POST", UriTemplate = "films", ResponseFormat = WebMessageFormat.Xml, 
		RequestFormat = WebMessageFormat.Xml)]
        void CreateFilm(Film newFilm);

        [OperationContract]
        [WebInvoke(Method = "PUT", UriTemplate = "films/{filmId}", RequestFormat = WebMessageFormat.Xml)]
        void UpdateFilm(string filmId, Film updatedFilm);

        [OperationContract]
        [WebInvoke(Method = "DELETE", UriTemplate = "films/{filmId}")]
        void DeleteFilm(string filmId);
    }
}
La seule nouveauté réside dans l'apparition des attributs WebGet et WebInvoke. Cependant, si vous avez bien suivi le début de l'article vous devez maintenant connaitre leur signification.

La fonction GetIndex va être utilisée pour accéder à la page html d'aide. Le point intéressant ici est le type d'objet que retourne cette fonction: Stream.

Les trois dernières opérations ne retournent pas de résultat (en apparence). On pourrait s'attendre à ce qu'elles renvoient quelque chose (un booléen par exemple) indiquant le bon déroulement ou non de l'opération. Cela n'est cependant pas utile ici. Nous utiliserons en effet les codes de statut HTTP (introduits au début de l'article) pour réaliser cela.


III-C. L'implémentation du contrat

Après le contrat, voyons maintenant son implémentation.

Nous créons donc une classe BoxOfficeREST implémentant l'interface IBoxOfficeREST. Nous n'utiliserons pas de base de données pour stocker les films, ceux-ci seront directement gérés dans une liste générique. De plus, par soucis de simplicité, nous configurerons le service en mode concurrentiel "Single" afin de ne pas avoir à gérer pas les accès concurrentiels pouvant intervenir sur cette liste.
Squelette de l'implémentation du contrat
namespace WcfServiceRestBoxOffice
{
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Single)]
    public class BoxOfficeREST : IBoxOfficeREST
    {
        private List<Film> listeFilms = null;
        //implémentation des méthodes de l'interface.
    }
}

Nous créons ensuite un accesseur pour la liste de films. Cela permet de créer la liste et de la remplir si cela n'avait pas encore été fait:
Accesseur de listeFilms
private List<Film> ListeFilms
{
    get 
    {
        if (listeFilms == null) //si la liste est null on la crée
        {
            //HTTP://fr.wikipedia.org/wiki/Box-office_fran%C3%A7ais
            listeFilms = new List<Film> { 
                new Film { Id = 1, Titre = "Titanic", Annee = 1997, Entrees = 20.64},
                new Film { Id = 2, Titre = "La Grande Vadrouille", Annee = 1968, Entrees = 17.27},
                //etc.
            };

            //on crée l'uri pour accéder à la ressource
            foreach (Film film in listeFilms)
            {
                film.Uri = new System.Uri(
				System.ServiceModel.OperationContext.Current.EndpointDispatcher.EndpointAddress.Uri.ToString()
				 + "/films/" + film.Id.ToString());
            }
        }
        return listeFilms; 
    }
}
Le point intéressant ici est la création de l'URI de chaque film. System.ServiceModel.OperationContext.Current.EndpointDispatcher.EndpointAddress.Uri permet de récupérer l'URI du EndPoint WCF de notre service. C'est-à-dire quelque chose du style HTTP://localhost:59000/BoxOfficeREST.svc. Nous rajoutons à la fin /films/idFilm afin de correspondre au modèle d'URI que nous avions fixé dans le contrat du service. Ainsi l'URI du film d'identifiant 1 sera au final HTTP://localhost:59000/BoxOfficeREST.svc/films/1.


III-C-1. GetIndex

Passons maintenant à l'implémentation de la fonction GetIndex, renvoyant la page d'aide:
Fonction GetIndex
public Stream GetIndex()
{
    MemoryStream stream = new MemoryStream();

    StreamWriter writer = new StreamWriter(stream, System.Text.Encoding.UTF8);
    writer.Write(Properties.Resources.filmsserviceindex);
    writer.Flush();

    stream.Position = 0;
    //on indique le type de contenu
    WebOperationContext.Current.OutgoingResponse.ContentType = "text/html";
    return stream;
}
Nous déclarons un objet de type MemoryStream dans lequel nous écrivons la page html se trouvant en ressources. Puis nous renvoyont ce flux d'octets. Toute l'astuce réside dans l'instruction WebOperationContext.Current.OutgoingResponse.ContentType = "text/html". Elle permet en effet de spécifier, au niveau de l'en-tête HTTP que le type de contenu de la réponse est de type html. Cela permet au client de savoir comment interpréter le contenu de la réponse. Car au final, que va récupérer le client ? Un flux d'octets contenant du code html. Mais si le serveur ne lui dit pas qu'il s'agit de code html, le client ne saura pas comment le lire. Un navigateur Web, par exemple, pourra alors afficher la page html correctement car il saura de quoi il s'agit.

Ouvrons justement un navigateur et rendons-nous à l'adresse HTTP://localhost:59000/BoxOfficeREST.svc/aide. Nous obtenons alors le résultat ci-dessous:

pageaide.png

Le service a envoyé du contenu de type HTML directement interprétable par le client. En utilisant la même technique (envoi de flux d'octets) vous pouvez renvoyer à peu près n'importe quoi: image, son, fichiers divers, etc. Le tout est de bien spécifier le type de contenu retourné afin que le client sache comment le traiter.

Par exemple, si vous renvoyez une image au format PNG vous devrez alors spécifier un type "image/png".

Bien, assez joué avec la page d'aide, passons maintenant à nos films.


III-C-2. GetFilm

Voici tout d'abord l'implémentation de la fonction GetFilm, qui renvoie un film en fonction de son identifiant. Rappelons que cette fonction sera appelée via une requête GET sur une URI du type HTTP://localhost:59000/BoxOfficeREST.svc/films/filmIdfilmId est l'identifiant du film.
Fonction GetFilm
public Film GetFilm(string filmId)
{
    int filmIdInt;
    Film film = null;

    if (!int.TryParse(filmId, out filmIdInt)) //si filmId n'est pas un int
    {
        WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HTTPStatusCode.BadRequest;
        return null;
    }
    
    film = ListeFilms.FirstOrDefault(f => f.Id == filmIdInt);

    if (film == null) //si le film n'existe pas
    {
        WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HTTPStatusCode.NotFound;
        return null;
    }

    WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HTTPStatusCode.OK;
    return film;
}
Comme souligné précédemment, les paramètres récupérés via l'URI sont des chaines de caractères. Il faut donc convertir la variable filmId en entier. Si la conversion échoue nous affectons la valeur BadRequest au code de statut HTTP de la réponse, puis nous quittons la fonction en retournant null.

Si filmId est bien un entier alors nous recherchons le film correspondant. Si le film n'existe pas alors nous affectons la valeur NotFound au code de statut HTTP de la réponse, puis nous quittons la fonction en retournant null. Si le film existe alors nous le renvoyons et affectons la valeur OK au code de statut HTTP de la réponse.

Grâce aux codes de statut HTTP le client peut ainsi connaître comment s'est déroulée la requête sans avoir besoin d'analyser le corps de la réponse. Nous verrons dans la deuxième partie de cet article comment un client (JavaScript et .NET) peut récupérer ces codes.


Si l'on ouvre un navigateur à l'adresse HTTP://localhost:59000/BoxOfficeREST.svc/films/1 alors nous obtenons le film d'identifiant 1 sérialisé en XML:

film1.png

Une requête à l'adresse HTTP://localhost:59000/BoxOfficeREST.svc/films/toto renverra une erreur 400 (toto n'étant pas un entier):

film400.png

Une requête à l'adresse HTTP://localhost:59000/BoxOfficeREST.svc/films/11 renverra une erreur 404 (le film d'identifiant 11 n'existant pas):

film404.png

III-C-3. GetFilms

Voyons maintenant la fonction GetFilms qui renvoie une liste de films (un objet de type ListeFilms) et prenant en charge la pagination.

L'accès à la ressource "liste des films" se fera via une URI de type HTTP://localhost:59000/BoxOfficeREST.svc/films. Le service doit aussi être en mesure de prendre en compte des paramètres "skip" et "take" permettant d'utiliser un système de pagination. Par exemple, une requête GET à l'URI HTTP://localhost:59000/BoxOfficeREST.svc/films?skip=1&take=3 renverra une liste contenant seulement les trois premiers films (par rapport au nombre d'entrées) en sautant le premier. Mais il doit également être possible de ne spécifier que le paramètre "skip" ou que le paramètre "take". Aucun autre paramètre ne doit être spécifié. Une URI du type HTTP://localhost:59000/BoxOfficeREST.svc/films?skip=1&take=3&toto=1 doit renvoyer une erreur.

Bref, comme vous le voyez il y a pas mal de vérifications à effectuer au niveau des paramètres.

La première chose à faire est de vérifier que les paramètres de l'URI font parti de la liste des paramètres autorisés (c'est-à-dire "skip" et "take"). Nous allons donc déclarer une collection générique paramsAutorisesGetFilms de type HashSet contenant la liste des paramètres possibles. Nous utiliserons aussi une fonction VerifierParamsGetfilms qui vérifiera que la liste des paramètres possibles est un sur-ensemble de la liste des paramètres de l'URI.
//liste des paramètres autorisés dans l'URI ListeFilms
private HashSet<String> paramsAutorisesGetFilms;

public HashSet<String> ParamsAutorisesGetFilms
{
    get 
    {
        if (paramsAutorisesGetFilms == null)
        {
            paramsAutorisesGetFilms = new HashSet<String>(new String[] { "skip", "take" }, 
				StringComparer.InvariantCultureIgnoreCase);
        }
        return paramsAutorisesGetFilms; 
    }
}

//renvoie true sur la liste des paramètres autorisés est un sur-ensemble de la liste des paramètres reçus
//HTTP://badger.developpez.com/tutoriels/dotnet/hashset/
private bool VerifierParamsGetfilms()
{
    WebOperationContext context = WebOperationContext.Current;
    UriTemplateMatch uriMatch = context.IncomingRequest.UriTemplateMatch;
    return ParamsAutorisesGetFilms.IsSupersetOf(uriMatch.QueryParameters.AllKeys);
}
Nous récupérons tout d'abord le contexte d'opération courant pour la requête entrante. Celui-ci possède une propriété UriTemplateMatch permettant d'accéder à une instance de UriTemplateMatch. Cette dernière dispose d'une propriété QueryParameters qui n'est rien d'autre qu'une collection de type clef/valeur dont les clefs sont les noms des paramètres de l'URI (par exemple "skip") et les valeurs sont les valeurs de ces paramètres (par exemple "1"). Voilà comment récupérer simplement les paramètres ainsi que leurs valeurs associées.

Reste maintenant à vérifier que les noms de ces paramètres font partie de la liste des paramètres autorisés. C'est-à-dire vérifier que la liste ParamsAutorisesGetFilms est un sur-ensemble de la liste QueryParameters.AllKeys. C'est là qu'intervient une autre nouveauté du Framework 3.5: la classe HashSet dont vous trouvez un article ici. Celle-ci permet d'effectuer des opérations d'ensembles comme l'union, l'intersection ou la différence symétrique. Elle possède une méthode IsSupersetOf qui renvoie vrai si l'objet HashSet est un sur-ensemble d'une liste passée en paramètre.


Nous allons ensuite créer une méthode TryGetIntFromQueryString ayant un fonctionnement similaire aux méthodes TryParse que l'on rencontre dans le Framework .NET. Elle permettra de convertir la valeur d'un paramètre de l'URI (qui se trouve sous forme de chaine de caractères) en un entier et de renvoyer un booléen indiquant si l'opération s'est bien déroulée ou non.
//affecte à "value" la valeur du paramètre "name" et renvoie true si réussite
private bool TryGetIntFromQueryString(string name, out int value)
{
    value = 0;
    WebOperationContext context = WebOperationContext.Current;
    UriTemplateMatch uriMatch = context.IncomingRequest.UriTemplateMatch;
    string strValue = uriMatch.QueryParameters[name];

    if (!String.IsNullOrEmpty(strValue))
    {
        return Int32.TryParse(strValue, out value);
    }
    return false;
}

Nous pouvons donc maintenant passer au code la fonction GetFilms:
Fonction GetFilms
public ListeFilms GetFilms()
{
    //on vérifie la validité des noms des paramètres
    if (!VerifierParamsGetfilms())
    {
        WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HTTPStatusCode.BadRequest;
        return null;
    }

    int skipInt;
    int takeInt;
   //s'il n'y a pas de paramètre "skip"
    if (!TryGetIntFromQueryString("skip", out skipInt))  
    {
        skipInt = 0; //valeur par défaut
    }
    //s'il n'y a pas de paramètre "take"
    if (!TryGetIntFromQueryString("take", out takeInt))
    {
        takeInt = ListeFilms.Count - skipInt; //valeur par défaut
    }

    //on vérifie la validité des valeurs des paramètres
    if ((skipInt >= 0) && (skipInt < ListeFilms.Count) && (takeInt > 0) && (takeInt <= ListeFilms.Count)
	 && (skipInt + takeInt <= ListeFilms.Count))
    {
        ListeFilms lf = new ListeFilms();
        lf.Films = ListeFilms.Skip<Film>(skipInt).Take<Film>(takeInt).OrderByDescending(f => f.Entrees).ToList();
        lf.Start = skipInt + 1;
        lf.TotalCount = ListeFilms.Count;
        lf.TakeCount = lf.Films.Count;

        WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HTTPStatusCode.OK;
        return lf;
    }
    else //mauvaise requête
    {
        WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HTTPStatusCode.BadRequest;
        return null;
    }
}
Nous vérifions tout d'abord les noms des paramètres de l'URI grâce à la méthode VerifierParamsGetfilms. S'ils ne sont pas valides nous renvoyons le code d'erreur BadRequest.

Nous essayons ensuite de convertir la valeur de ses paramètres en entier. Une valeur par défaut est éventuellement utilisée.

Puis nous vérifions la validité des valeurs des paramètres. Par exemple, une valeur de 20 pour le paramètre skip serait incorrecte s'il n'y a que 10 films disponibles.

Enfin nous créons une instance de ListeFilms et nous affectons ses propriétés avec les valeurs adéquates. Nous retournons cet objet avec le code de statut HTTP OK.

Un petit test dans un navigateur nous donne le résultat suivant pour l'URI HTTP://localhost:59000/BoxOfficeREST.svc/films?skip=1&take=2:

listefilms1.png

III-C-4. CreateFilm

L'ajout d'un nouveau film se fera via la méthode CreateFilm. Celle-ci sera appelée lorsque le service recevra une requête HTTP POST sur l'URI HTTP://localhost:59000/BoxOfficeREST.svc/films. Le corps de cette requête contiendra le nouveau film à ajouter, sérialisé en XML. WCF va automatiquement dé-sérialiser ces données en un objet de type Film et passer celui-ci en paramètre de la méthode CreateFilm. Si la dé-sérialisation échoue, le paramètre aura pour valeur null.
Fonction CreateFilm
public void CreateFilm(Film newFilm)
{    
    if (newFilm != null)
    {
        //calcul du nouvel identifiant
        newFilm.Id = ListeFilms.Max(f => f.Id) + 1;

        System.UriTemplateMatch match = WebOperationContext.Current.IncomingRequest.UriTemplateMatch;
        System.UriTemplate template = new System.UriTemplate("/films/{id}");
        newFilm.Uri = template.BindByPosition(match.BaseUri, newFilm.Id.ToString());

        ListeFilms.Add(newFilm);

        //on affecte la valeur du champ d'entête HTTP "Location" avec l'uri du film créé.
		WebOperationContext.Current.OutgoingResponse.SetStatusAsCreated(newFilm.Uri);
    }
    else //erreur dans la requête
    {
        WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HTTPStatusCode.BadRequest;
    }
}
Comme vous le voyez, le code est assez simple. Nous vérifions tout d'abord que la dé-sérialisation s'est bien déroulée. Dans le cas contraire nous renvoyons le code de statut BadRequest.

Nous calculons le nouvel identifiant du film, construisons son URI (dans notre exemple, ces deux propriétés ne peuvent pas être attribuées par le client) et ajoutons le film à la liste des films.

Il nous faut ensuite renvoyer deux informations importantes au client. La première est le fait que l'ajout s'est bien déroulé, c'est-à-dire envoyer le code de statut Created. La deuxième est l'URI du film nouvellement ajouté. Pourquoi l'URI ? Pour que le client sache comment accéder au film. Tout cela se fait en une seule instruction grâce à la méthode SetStatusAsCreated. Celle-ci assigne le code de statut HTTP de la réponse à Created et assigne le champ d'entête HTTP Location avec une URI passée en paramètre.

Nous verrons plus tard comment le client peut récupérer ces informations contenues dans la réponse HTTP du service.


III-C-5. UpdateFilm

La méthode UpdateFilm permettant de modifier un film existant sera appelée quand le service recevra une requête HTTP PUT sur une URI du type HTTP://localhost:59000/BoxOfficeRest.svc/films/FilmIdFilmId est l