Introduction aux services web REST avec WCF 3.5

Florian Casabianca

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

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Introduction aux services web REST avec WCF 3.5

- Auteur : Florian Casabianca

- Parution : 25 mai 2008

- Mise à jour : 25 mai 2008

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2008 Florian Casabianca. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- 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'attente, 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éresser, 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 surfez 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 entê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'entê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 importants 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 contenues 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 :

Il est ainsi très facile d'extraire les valeurs de variables contenues dans une URI dont on a défini le modèle.

À 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 prises 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 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 prises 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. À 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 entê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 entê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

WebHTTPBinding 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 :

- webHTTP - 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 lorsqu'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 suivants, il faudra indiquer :

- nombre de films à inclure : 10 ;

- nombre de films à « sauter » : 10.

À 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 incluse 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 :

Ressource

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 :

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 souci 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çais
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 finalement 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 renvoyons 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'entê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 finalement, 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 :

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/filmId où filmId 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 connaitre 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 :

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

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

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 partie 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 surensemble 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 surensemble 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 listeParamsAutorisesGetFilms est un surensemble de la listeQueryParameters.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 surensemble 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 :

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/FilmId où FilmId est l'identifiant du film à modifier.

Cette méthode prend deux paramètres en entrée. Le premier est l'identifiant du film à modifier qui sera récupéré à partir de l'URI. Le deuxième est un objet de type Film correspondant à la nouvelle version du film. Cette nouvelle version sera transmise par le client dans le corps de la requête HTTP PUT et sérialisé en XML. WCF la désérialisera automatiquement et créera une instance de type Film qui sera passée en argument de la méthode UpdateFilm.

Fonction UpdateFilm

public void UpdateFilm(string filmId, Film updatedFilm)
{
int filmIdInt;
Film film = null;

//on parse l'identifiant envoyé en entier
if (int.TryParse(filmId, out filmIdInt))
{
//on recherche le film à modifier
film = ListeFilms.FirstOrDefault(f => f.Id == filmIdInt);
//s'il existe on le modifie et on renvoie le statut Created
if (film != null)
{
film.Titre = updatedFilm.Titre;
film.Annee = updatedFilm.Annee;
film.Entrees = updatedFilm.Entrees;
WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HTTPStatusCode.OK;
}
else //sinon on envoie une erreur
{
WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HTTPStatusCode.NotFound;
}
}
else  //si l'identifiant n'est pas un entier
{
WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HTTPStatusCode.BadRequest;
}
}

Le code de la méthode est assez classique. Nous convertissons tout d'abord le paramètre filmId en entier (comme d'habitude). Nous recherchons ensuite le film correspondant à cet identifiant dans la liste des films disponibles et mettons à jour les données le concernant.

N'oublions pas le code de statut HTTP qui sera renvoyé dans la réponse : OK si la modification s'est bien déroulée, NotFound si filmId ne correspond à aucun film et BadRequest si filmId n'est pas un entier.

III-C-6 - DeleteFilm

La suppression d'un film se fera via l'envoi d'une requête HTTP DELETE sur une URI du type HTTP://localhost:59000/BoxOfficeRest.svc/films/FilmId où FilmId est l'identifiant du film à supprimer. Le client n'a pas ici besoin de passer une quelconque information dans le corps de la requête. Seuls l'URI de la ressource et le verbe HTTP DELETE suffisent à exprimer l'action à effectuer.

Une telle requête conduira à l'appel de la méthode DeleteFilm. Cette dernière prend en paramètre l'identifiant du film à supprimer (issue de l'URI).

Fonction DeleteFilm

public void DeleteFilm(string filmId)
{
int filmIdInt;
Film film = null;

//on parse l'identifiant envoyé en entier
if (int.TryParse(filmId, out filmIdInt))
{
//on recherche le film à supprimer
film = ListeFilms.FirstOrDefault(f => f.Id == filmIdInt);
//s'il existe on le supprime et on renvoie le statut OK
if (film != null)
{
ListeFilms.Remove(film);
WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HTTPStatusCode.OK;
}
else //sinon on envoie une erreur
{
WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HTTPStatusCode.NotFound;
}
}
else //si l'identifiant n'est pas un entier
{
WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HTTPStatusCode.BadRequest;
}
}

Après la traditionnelle conversion de l'identifiant en entier nous recherchons le film correspondant. S'il existe alors nous le supprimons et renvoyons le code de statut OK.

III-D - Test du service avec Fiddler

Comme vous avez pu le voir, l'accès aux ressources via une requête HTTP GET peut se tester assez facilement avec un navigateur web. Ce n'est par contre pas le cas avec les autres verbes HTTP (POST, PUT et DELETE) où il faut construire soit même la requête HTTP.

Pour nous aider, il existe un outil nommé Fiddler (HTTP://www.fiddler2.com/fiddler2/) développé par un employé de Microsoft. Fiddler permet d'analyser le trafic HTTP entrant et sortant de votre machine.

Commençons par quelque chose de simple : une requête HTTP GET pour obtenir les données du film d'identifiant 1. Dans l'interface de Fiddler ouvrez l'onglet Request Builder. Une liste déroulante vous permet de sélectionner le verbe HTTP de la requête, sélectionnez donc le verbe GET. Dans la zone de texte, à droite saisissez l'URI sur laquelle effectuer la requête et cliquez sur le bouton Execute.

Dans le panneau de gauche, vous devriez voir la réponse envoyée par le service :

Sélectionnez-la et ouvrez l'onglet Session Inspector. Celui se découpe en deux panneaux. Celui du haut montre la requête envoyée et celui du bas montre la réponse reçue :

Vous pouvez voir que le corps de la réponse contient le film d'identifiant 1 sérialisé en XML. Les champs d'entête permettent de voir notamment le code de statut renvoyé (OK) ainsi que le type de contenu (XML).

Essayons maintenant d'ajouter un nouveau film. Dans l'onglet Request Builder nous sélectionnons le verbe HTTP POST et l'URI HTTP://127.0.0.1:59000/BoxOfficeREST.svc/films.

Dans le panneau des entêtes de la requête HTTP il est important de ne pas oublier le type de contenu (application/xml). Le champ Content-Length sera automatiquement calculé par Fiddler.

Dans le panneau dédié au corps de la requête, nous devons saisir les données du nouveau film à ajouter sous format XML :

Il n'est pas nécessaire de spécifier l'identifiant ni l'URI du film étant donné qu'il s'agit de valeurs calculées par le service.

Une fois la requête envoyée nous pouvons visualiser la réponse du service en la sélectionnant dans le panneau de gauche et en affichant l'onglet Session Inspector :

L'entête de la réponse comporte les deux informations attendues : le code de statut Created indiquant le succès de la requête et le champ Location contenant l'URI du nouveau film.

Arrêtons là pour ces quelques tests. Nous n'avons pas testé la modification ou la suppression d'un film, mais celles-ci se font en utilisant les mêmes techniques que précédemment.

Comme vous venez de le voir, Fiddler est un outil très intéressant pour (en autres) tester les services REST. N'hésitez donc pas à vous en servir!

III-E - L'hébergement du service

Afin d'utiliser notre service nous allons l'héberger au sein d'une application web (un simple projet de type ASP.NET web Application).

Après avoir ajouté une référence au service WCF il faut créer un fichier BoxOfficeREST.svc contenant le code suivant :

<%@ ServiceHost Language="C#" Debug="true" Service="WcfServiceRestBoxOffice.BoxOfficeREST" %>

Puis dans le fichier de configuration Web.config rajouter le code suivant :

Configuration XML du service

<system.serviceModel>
<services>
<service name="WcfServiceRestBoxOffice.BoxOfficeREST">
<endpoint address=""
binding="webHttpBinding"
contract="WcfServiceRestBoxOffice.IBoxOfficeREST"
behaviorConfiguration="BoxOfficeREST.FilmsBehavior"/>
</service>
</services>
<behaviors>
<endpointBehaviors>
<behavior name="BoxOfficeREST.FilmsBehavior">
<webHttp/>
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>

Il ne s'agit que de manipulations classiques pour des services WCF.

Deux points sont à noter concernant la configuration du service. Le premier est le binding utilisé : webHTTPBinding. Il s'agit du nouveau binding apporté par WCF 3.5 que nous avons évoqué en début d'article. Le deuxième est l'élément <webHTTP/> qui, employé conjointement avec webHTTPBinding, permet à WCF d'exposer et d'utiliser des services de style web.

Comme vous le savez sans doute, Visual Studio embarque un mini serveur web (représenté par cette icône dans la barre des tâches) et permettant de tester des applications web sans avoir besoin de les déployer dans IIS (par exemple). Le souci dans notre cas est que le numéro du port utilisé par ce serveur pour héberger notre application web change à chaque fois que l'on relance le serveur.

Pour tester notre service, il serait plus simple d'avoir un numéro de port fixe. Pour réaliser cela, il suffit de se rendre dans les propriétés de l'application web, puis dans l'onglet web. Il ne reste plus qu'à choisir l'option « port spécifique » et de saisir le numéro désiré :

Dans notre exemple nous utilisons le numéro de port 59000.

Ici s'achève la partie service de cet article. Nous allons maintenant nous intéresser à la construction de clients venant consommer ce service REST avec les exemples d'un client JavaScript et d'un client .NET.

IV - Client JavaScript

IV-A - L'objet XMLHttpRequest

XMLHttpRequest est un objet JavaScript introduit par Microsoft dans Internet Explorer et maintenant présent dans tous les navigateurs modernes. Il permet d'effectuer des requêtes HTTP et d'en récupérer le résultat de manière asynchrone. Il est utilisé dans la technique de développement Ajax afin de mettre à jour les données contenues dans une page sans devoir la recharger.

Cette classe n'est pas standardisée et son utilisation peut varier selon les navigateurs. La première chose à faire est donc de créer une fonction qui va construire un objet XMLHttpRequest en fonction du navigateur :

Création d'un objet XMLHttpRequest

function getHTTPObject() //permet de construire un objet XMLHttpRequest
{
var xmlhttp = false;

if (window.XMLHttpRequest)
{
//Firefox ou IE >= 7.0
var xmlhttp = new XMLHttpRequest();
}
else if (window.ActiveXObject)
{
try
{ // essaie de charger l'objet pour IE
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e)
{
try
{ // essaie de charger l'objet pour une autre version IE
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
catch (e)
{
xmlhttp = false;
window.alert("Votre navigateur ne prend pas en charge l'objet XMLHTTPRequest.");
}
}
}
return xmlhttp;
}

Cette fonction renvoie un objet XMLHttpRequest en cas de succès et la valeur false dans le cas contraire.

IV-B - Récupérer un film

L'accès aux informations concernant un film se fait en envoyant une requête HTTP GET sur une URI de type http://localhost:59000/BoxOfficeREST.svc/films/idFilm où idFilm correspond à l'identifiant du film désiré.

Récupérer un film

function ButtonGET_onclick() {
var xmlget = getHTTPObject(); //création d'un objet XMLHttpRequest
if(!xmlget){ return; }

//fonction de rappel
xmlget.onreadystatechange  = function()
{
if(xmlget.readyState == 4) //4 = terminé. Les données sont chargées.
{
if(xmlget.status  == 200)
document.getElementById("TextAreaGET").value = xmlget.responseText;
else
alert("Error code " + xmlget.status + xmlget.responseText);
}
};
/récupération de l'identifiant du film
var idFilm = document.getElementById("idGET").value;

xmlget.open("GET", "http://localhost:59000/BoxOfficeREST.svc/films/"+idFilm, true);
xmlget.send(null); //envoie de la requête
}

Nous créons tout d'abord un objet XMLHttpRequest grâce à notre méthode getHTTPObject. L'objet XMLHttpRequest possède une propriété onreadystatechange à laquelle on associe une fonction de rappel. Cette fonction sera appelée à chaque changement de statut de l'objet. Le statut est accessible via la propriété readyState. Les différents codes possibles pour le statut de l'objet sont :

- 0 = non initialisé ;

- 1 = ouverture. La méthode open() a été appelée avec succès ;

- 2 = envoyé. La méthode send() a été appelée avec succès ;

- 3 = en train de recevoir. Des données sont en train d'être transférées, mais le transfert n'est pas terminé ;

- 4 = terminé. Les données sont chargées.

Dans la fonction de rappel, nous attendons le code 4 indiquant que la réponse du serveur a été entièrement reçue. Nous analysons ensuite la propriété status qui contient le code de statut HTTP contenu dans l'entête de la réponse. Nous avions configuré notre service pour renvoyer le code OK (200) en cas de succès. Ainsi, si nous récupérons ce code nous sommes sûrs d'avoir bien obtenu les données du film désiré. Ces données sont disponibles via la propriété responseText de l'objet XMLHttpRequest. Enfin nous les affichons dans un élément TextArea.

Une fois la fonction de rappel configurée, il ne reste plus qu'à créer la requête. Nous récupérons tout d'abord l'identifiant du film saisi dans une zone de texte (idGET). Nous appelons ensuite la méthode open de l'objet XMLHttpRequest. Cette méthode prépare la requête et prend en paramètre la méthode HTTP à utiliser (ici GET), l'URL et un booléen indiquant si la requête s'effectue de façon synchrone (false) ou asynchrone (true). Il est préférable d'effectuer des requêtes asynchrones. Enfin nous appelons la méthode send qui comme son nom l'indique va envoyer la requête. Cette méthode prend en paramètre une chaine de caractères qui correspond aux données envoyées au serveur. Dans le cas d'une requête GET nous n'envoyons aucune donnée et mettons en paramètre « null ».

IV-C - Supprimer un film

La suppression d'un film se fait par l'intermédiaire d'une requête HTTP DELETE sur une URI de type http://localhost:59000/BoxOfficeREST.svc/films/idFilm où idFilm est l'identifiant du film à supprimer. Notre service renvoie le code de statut HTTP OK (200) si la suppression s'est déroulée correctement. Voici le code correspondant :

Supprimer un film

function ButtonDELETE_onclick() {
var xmldelete = getHTTPObject(); //création d'un objet XMLHttpRequest
if(!xmldelete){ return; }

xmldelete.onreadystatechange  = function()
{
if(xmldelete.readyState == 4)
{
if(xmldelete.status  == 200)
alert("Film effacé");
else
alert("Error code " + xmldelete.status);
}
};

var idFilm = document.getElementById("IdDELETE").value;

xmldelete.open("DELETE", "http://localhost:59000/BoxOfficeREST.svc/films/"+idFilm, true);
xmldelete.send(null);
}

Comme vous le voyez, il s'agit du même type de code que précédemment. Il est donc inutile de revenir dessus.

IV-D - Ajouter un film

Pour ajouter un nouveau film nous devons envoyer une requête HTTP POST sur l'URI http://localhost:59000/BoxOfficeREST.svc/films. Le corps de cette requête devra contenir la représentation XML de ce nouveau film.

Ajouter un film

function ButtonPOST_onclick() {
var xmlpost = getHTTPObject();//création d'un objet XMLHttpRequest
if(!xmlpost){ return; }

var postdata = document.getElementById("TextAreaPOST").value;

xmlpost.onreadystatechange  = function()
{
if(xmlpost.readyState == 4)
{
if(xmlpost.status  == 201)
//on affiche la valeur du champ d'entête HTTP "Location"
document.getElementById("TextAreaPOSTRetour").value = xmlpost.getResponseHeader("Location");
else
alert("Error code " + xmlpost.status + xmlpost.responseText);
}
};

xmlpost.open("POST", "http://localhost:59000/BoxOfficeREST.svc/films", true);
xmlpost.setRequestHeader('Content-Type', 'text/xml');
xmlpost.send(postdata);
}

La structure du code est identique que précédemment (création de l'objet XMLHttpRequest, fonction de rappel, etc.). Il y a cependant trois nouveautés ici. La première est l'utilisation de la méthode setRequestHeader de l'objet XMLHttpRequest qui permet d'assigner une valeur à un champ d'entête HTTP. Dans notre cas nous spécifions que le corps de la requête sera de type XML. La deuxième nouveauté est le passage d'un paramètre à la méthode send. Il s'agit des données que nous souhaitons envoyer avec la requête (c'est-à-dire les données du nouveau film au format XML). Nous récupérons ces données depuis une zone de texte TextAreaPOST. Enfin la troisième nouveauté se situe dans la fonction de rappel. Nous avions configuré notre service pour que dans sa réponse HTTP (suite à l'ajout d'un film) il spécifie dans le champ d'entête Location l'URI qu'il aura assigné au nouveau film. C'est donc cette valeur que nous allons récupérer grâce à la fonction getResponseHeader.

IV-E - Modifier un film

La modification d'un film passe par une requête HTTP PUT envoyée sur une URI de type http://localhost:59000/BoxOfficeREST.svc/films/idFilm où idFilm est l'identifiant du film à modifier. Le corps de cette requête contiendra la nouvelle représentation du film au format XML.

Modifier un film

function ButtonPUT_onclick() {
var xmlput = getHTTPObject();//création d'un objet XMLHttpRequest
if(!xmlput){ return;

var putdata = document.getElementById("TextAreaPUT").value;

xmlput.onreadystatechange  = function()
{
if(xmlput.readyState == 4)
{
if(xmlput.status  == 201)
alert("Film modifié");
else
alert("Error code " + xmlput.status + xmlput.responseText);
}
};

var idFilm = document.getElementById("idPUT").value;

xmlput.open("PUT", "http://localhost:59000/BoxOfficeREST.svc/films/"+idFilm, true);
xmlput.setRequestHeader('Content-Type', 'text/xml');
xmlput.send(putdata);
}

Comme vous le voyez, le code est sensiblement identique au code d'ajout d'un nouveau film.

V - Client .NET

Nous allons maintenant passer au développement d'une application cliente en .NET. Le but est de découvrir le code nécessaire à la création et l'envoi de requêtes HTTP ainsi que la récupération des informations contenues dans les réponses du service.

Pour cela nous allons créer une application WinForm au design novateur comme vous pouvez le voir sur l'image ci-dessous :

Le projet ciblera le Framework 3.5, ce qui nous amènera à rencontrer quelques nouveautés du C# 3.0 ainsi que l'utilisation de LINQ to XML. Mais vous pouvez parfaitement utiliser une version antérieure du Framework pour développer un client REST, le code sera pratiquement le même.

Nous déclarons une constante UrlWebService qui contiendra l'adresse du service REST et dont nous nous servirons dans le reste du code :

private const String UrlWebService = @"http://localhost:59000/BoxOfficeREST.svc";

V-A - Récupérer un film

Comme vous le savez maintenant, l'obtention de la représentation XML d'un film se fait en envoyant une requête HTTP GET sur une URI de type http://localhost:59000/BoxOfficeREST.svc/films/idFilm où idFilm correspond à l'identifiant du film désiré.

La création d'une telle requête se fait en deux lignes de code :

private void btnGetFilm_Click(object sender, EventArgs e)
{
HttpWebRequest requete = WebRequest.Create(UrlWebService + "/films/" + txtIdFilm.Text) as HttpWebRequest;
requete.Method = WebRequestMethods.Http.Get;

//requete asynchrone
requete.BeginGetResponse(new AsyncCallback(this.GetFilmResponseCallback), requete);
}

Nous appelons la fonction Create de la classe WebRequest en lui passant en paramètre l'URI de destination. Celle-ci renvoie un objet de type WebRequest que nous castons en un objet de type HttpWebRequest. Nous spécifions ensuite le type de requête (ici GET) via la propriété Method.

Ne reste plus qu'à envoyer la requête au service. Afin de ne pas bloquer l'interface graphique en attendant la réponse du service, nous allons utiliser la fonction asynchrone BeginGetResponse. Celle-ci prend en paramètre l'adresse d'une méthode qui sera appelée lorsque la réponse sera arrivée ainsi qu'un objet auquel vous souhaitez pouvoir accéder au sein de cette méthode (qui s'effectuera sur un thread différent du thread principal). Ici nous passons notre objet HttpWebRequest.

Pour en savoir plus sur l'envoi de requêtes HTTP en asynchrone vous pouvez lire le tutoriel Récupérer un flux web de manière asynchrone en C# de Pierre Jallais.

Revenons à notre méthode de rappel. Celle-ci reçoit en paramètre un objet de type IAsyncResult dont la propriété AsyncState contient l'objet HttpWebRequest que nous avions passé lors de l'appel à la méthode BeginGetResponse.

Nous pouvons ainsi récupérer la réponse HTTP du service à notre requête via la méthode EndGetResponse.

private void GetFilmResponseCallback(IAsyncResult ar)
{
//Récupération de l'objet HttpWebRequest
HttpWebRequest requete = (HttpWebRequest)ar.AsyncState;

try
{
using (HttpWebResponse reponse = requete.EndGetResponse(ar) as HttpWebResponse)
{
using (StreamReader streamReader = new StreamReader(reponse.GetResponseStream(), System.Text.Encoding.UTF8))
{
XElement ex = XElement.Load(XmlReader.Create(streamReader));

//remplissage du richTextBox
richTextBoxFilmXml.Invoke((MethodInvoker)delegate
{
richTextBoxFilmXml.Text = ex.ToString();
});
}
}
}
catch (WebException we)
{
if (we.Status == WebExceptionStatus.ProtocolError)
{
HttpWebResponse r = (HttpWebResponse)we.Response;
if (r.StatusCode == HttpStatusCode.NotFound)

MessageBox.Show("Code d'erreur: " + r.StatusCode.ToString());
r.Close();
}
else
MessageBox.Show(we.Message + " " + we.Status.ToString());
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Erreur");
}
}

La lecture du corps de la réponse se fait en appelant la méthode GetResponseStream. Celle-ci renvoie un flux (objet de type Stream) que nous utilisons pour créer un StreamReader. Grâce à ce dernier, nous chargeons un objet XElement (de l'espace de nom System.Xml.Linq). Vous pouvez ensuite utiliser ce XElement (contenant la représentation d'un film au format XML) afin d'effectuer les traitements que vous voulez. Dans notre cas nous allons simplement afficher son contenu dans une richtextBox.

Vous noterez que nous utilisons la méthode Invoke prenant en paramètre un delegate sur une méthode. Ceci est nécessaire, car, du fait de l'utilisation d'une requête asynchrone, nous nous trouvons sur un thread différent du thread principal (IHM) et nous ne pouvons pas modifier directement la propriété Text du richTextBox.

Analysons la capture des exceptions. Une exception de type WebException est soulevée si une erreur survient lors de l'accès au réseau. La propriété Status indique le statut de la réponse. Ici nous nous intéressons au cas où le statut traduit une erreur de niveau protocole (dans notre cas HTTP). C'est-à-dire que nous avons bien reçu une réponse du service, mais que cette réponse contenait un code d'erreur HTTP (par exemple 404). Pour récupérer ce code d'erreur, nous utilisons simplement la propriété StatusCode de l'objet HttpWebResponse.

Dans la suite de l'application, et afin de simplifier le code, nous n'utiliserons plus la méthode asynchrone BeginGetResponse, mais la méthode synchrone (et donc bloquante) GetResponse. Mais comprenez bien que l'utilisation de la méthode asynchrone est à privilégier (particulièrement si vous développez en Silverlight qui ne supporte pas les appels synchrones).

V-B - Récupérer la liste des films

Voyons maintenant comment récupérer une liste de films (requête HTTP GET). Afin de manipuler autre chose que simplement du XML, nous allons définir une classe FilmClient qui représentera un film côté client. C'est donc une classe différente de la classe Film que nous avions vue côté service. En voici sa définition :

Classe FilmClient

[System.Xml.Serialization.XmlRootAttribute(
Namespace = "http://schemas.developpez.com/BoxOfficeREST/2008/03",
IsNullable = false,
ElementName = "Film")]
public class FilmClient
{
public int Id { get; set; }

public string Titre { get; set; }

public int Annee { get; set; }

public double Entrees { get; set; }

public string Uri { get; set; }
}

L'attribut XmlRoot va nous être utile pour personnaliser la sérialisation de cette classe afin qu'elle respecte le schéma du service.

Passons maintenant au code :

Listes de films

private void btnGetFilms_Click(object sender, EventArgs e)
{
List<FilmClient> listeFilms;

// http://msdn2.microsoft.com/fr-fr/library/bb515701.aspx
UriTemplate template = new UriTemplate("films?skip={s}&take={t}");
Uri prefix = new Uri(UrlWebService);

NameValueCollection parameters = new NameValueCollection();
parameters.Add("s", txtSkip.Text);
parameters.Add("t", txtTake.Text);
Uri namedUri = template.BindByName(prefix, parameters);

//création de la requete
HttpWebRequest requete = WebRequest.Create(namedUri) as HttpWebRequest;
requete.Method = WebRequestMethods.Http.Get;

try
{
//on récupère la réponse du serveur
using (HttpWebResponse reponse = requete.GetResponse() as HttpWebResponse)
{
using (StreamReader streamReader = new StreamReader(reponse.GetResponseStream(), System.Text.Encoding.UTF8))
{
XElement ex = XElement.Load(XmlReader.Create(streamReader));
XNamespace ns = "http://schemas.developpez.com/BoxOfficeREST/2008/03";

//affichage du xml
richTextBoxFilmsXml.Text = ex.ToString();

listeFilms = (from f in ex.Descendants(ns + "Film")
select new FilmClient
{
Id = int.Parse(f.Element(ns + "Id").Value),
Titre = f.Element(ns + "Titre").Value,
Annee = int.Parse(f.Element(ns + "Annee").Value),
Entrees = double.Parse(f.Element(ns + "Entrees").Value,
System.Globalization.CultureInfo.GetCultureInfo("en-US").NumberFormat),
Uri = f.Element(ns + "Uri").Value
}).ToList();

dataGridViewFilms.DataSource = listeFilms;
}
}
}
catch (WebException we)
{
if (we.Status == WebExceptionStatus.ProtocolError)
{
HttpWebResponse r = (HttpWebResponse)we.Response;
MessageBox.Show(r.StatusCode.ToString());
r.Close();
}
else
MessageBox.Show(we.Message + " " + we.Status.ToString());
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Erreur");
}
}

Nous déclarons tout d'abord une liste générique d'objets FilmClient. Nous construisons ensuite l'URI sur laquelle nous enverrons la requête. Les valeurs des paramètres skip et take sont obtenues à partir de textBox de l'interface graphique.

Le code de la construction de la requête, la récupération de la réponse du service et de la création d'un objet XElement est identique à celui vu précédemment. Au lieu de simplement afficher le contenu du XElement à l'écran, nous allons l'utiliser pour créer des objets FilmClient et remplir la liste générique listeFilms. Pour cela nous utilisons une requête LINQ qui va itérer sur chaque élément Film du XML reçu et créer l'objet FilmClient correspondant. Enfin nous affectons la liste de films générée comme dataSource d'une gridView.

Le résultat en image :

V-C - Ajouter un film

Pour ajouter un film il faut envoyer une requête HTTP POST (contenant la représentation XML du nouveau film) sur l'URI de la liste des films.

Ajouter un film

private void btnCreateFilm_Click(object sender, EventArgs e)
{
//création de la requete
HttpWebRequest requete = WebRequest.Create(UrlWebService + "/films") as HttpWebRequest;
requete.Method = WebRequestMethods.Http.Post;
requete.ContentType = "text/xml";

//le film à ajouter
FilmClient film = new FilmClient
{
Annee = int.Parse(txtAnneeCreate.Text),
Entrees = double.Parse(txtEntreesCreate.Text),
Titre = txtTitreCreate.Text
};

// Écriture des données de la requête
Stream requeteStream = requete.GetRequestStream();
//sérialisation du film
System.Xml.Serialization.XmlSerializer writer = new System.Xml.Serialization.XmlSerializer(typeof(FilmClient));
writer.Serialize(requeteStream, film);
requeteStream.Close();

//affichage du XML envoyé dans une richTextBox
StringBuilder stb = new StringBuilder();
using (StringWriter sw = new StringWriter(stb))
{
writer.Serialize(sw, film);
}
richTextBoxCreateFilm.Text = stb.ToString();

try
{
//on récupère la réponse du serveur
using (HttpWebResponse reponse = requete.GetResponse() as HttpWebResponse)
{
//valeur du champ d'entête HTTP "Location"
String location = reponse.Headers["location"];

this.txtHttpLocation.Text = location;
}
}
catch (WebException we)
{
if (we.Status == WebExceptionStatus.ProtocolError)
{
HttpWebResponse r = (HttpWebResponse)we.Response;
MessageBox.Show("Code d'erreur: " + r.StatusCode.ToString());
r.Close();
}
else
MessageBox.Show(we.Message + " " + we.Status.ToString());
}
catch (Exception ex)
{
MessageBox.Show("Erreur", ex.Message);
}
}

Nous créons tout d'abord une requête HTTP comme nous en avons maintenant l'habitude en spécifiant la méthode et le type de contenu. On instancie ensuite un objet FilmClient qui correspond au film que l'on souhaite ajouter. Les propriétés de ce film (le titre, l'année et le nombre d'entrées) sont remplies avec des valeurs récupérées à partir de textBox au sein de l'interface graphique.

Il nous faut ensuite saisir dans le corps de la requête la représentation XML de ce film. Nous allons donc tout simplement sérialiser l'objet FilmClient en XML. Pour cela nous récupérons en premier un flux (Stream) grâce à la fonction GetRequestStream de l'objet HttpWebRequest permettant d'écrire les données envoyées avec la requête. Puis nous sérialisons le film en écrivant le résultat de cette sérialisation dans le flux obtenu précédemment.

Nous récupérons ensuite la réponse du service ainsi que la valeur de l'entête HTTP Location qui a été rempli par le service avec l'URI du film nouvellement créé.

Bien sûr nous gérons les cas d'erreurs comme vu précédemment.

Voici le résultat en image de l'ajout d'un nouveau film :

V-D - Modifier un film

Afin de modifier un film il faut envoyer une requête HTTP PUT sur l'URI du film que l'on souhaite changer.

Nous avions utilisé lors de l'ajout d'un film la sérialisation en XML afin de remplir le corps de la requête HTTP. Pour changer, nous allons ici voir une autre technique utilisant une chaine de caractère contenant le XML à envoyer.

Modifier un film

private void btnUpdateFilm_Click(object sender, EventArgs e)
{
String xml = "<Film xmlns=\"http://schemas.developpez.com/BoxOfficeREST/2008/03\"
xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">
<Id>{0}</Id><Titre>{1}</Titre><Annee>{2}</Annee><Entrees>{3}</Entrees></Film>";

String putXml = String.Format(xml, this.txtidFilmUpdate.Text,
this.txtTitreUpdate.Text, this.txtAnneeUpdate.Text, this.txtEntreesUpdate.Text);

richTextBoxUpdateFilm.Text = putXml;

//création de la requete
HttpWebRequest requete = WebRequest.Create(UrlWebService + "/films/" + this.txtidFilmUpdate.Text) as HttpWebRequest;
requete.Method = WebRequestMethods.Http.Put;
requete.ContentType = "text/xml";

UTF8Encoding encoding = new UTF8Encoding();
byte[] byte1 = encoding.GetBytes(putXml);

requete.ContentLength = byte1.Length;

// Écriture des données de la requête
Stream requeteStream = requete.GetRequestStream();
requeteStream.Write(byte1, 0, byte1.Length);
requeteStream.Close();

try
{
//on récupère la réponse du serveur
using (HttpWebResponse reponse = requete.GetResponse() as HttpWebResponse)
{
if (reponse.StatusCode == HttpStatusCode.OK)
MessageBox.Show("Film modifié");
}
}
catch (WebException we)
{
if (we.Status == WebExceptionStatus.ProtocolError)
{
HttpWebResponse r = (HttpWebResponse)we.Response;
MessageBox.Show("Code d'erreur: " + r.StatusCode.ToString());
r.Close();
}
else
MessageBox.Show(we.Message + " " + we.Status.ToString());
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Erreur");
}
}

Nous créons donc une variable putXml qui contient la représentation XML du film à modifier. Les valeurs du titre, de l'année de sortie et du nombre d'entrées sont récupérées depuis l'interface graphique.

Nous passons ensuite à la création de la requête avec spécification de l'URI, de la méthode (PUT) et du type de contenu.

Nous encodons le contenu de la variable putXml dans un tableau d'octets dont nous nous servons ensuite pour remplir le corps de la requête.

Le reste du code n'est que du classique : récupération de la réponse du service, analyse du code de statut HTTP, gestion des exceptions.

Voici l'interface graphique correspondante :

V-E - Supprimer un film

La suppression d'un film se fait en envoyant une requête HTTP DELETE sur l'URI du film à supprimer.

Suppression d'un film

private void btnDeleteFilm_Click(object sender, EventArgs e)
{
//création de la requete
HttpWebRequest requete = WebRequest.Create(UrlWebService+"/films/" + txtDeleteFilm.Text) as HttpWebRequest;
requete.Method = "DELETE";

try
{
using (HttpWebResponse reponse = requete.GetResponse() as HttpWebResponse) //on récupère la réponse du serveur
{
//statut OK, film supprimé
if (reponse.StatusCode == HttpStatusCode.OK)
MessageBox.Show("Film supprimé");
}
}
catch (WebException we)
{
if (we.Status == WebExceptionStatus.ProtocolError)
{
HttpWebResponse r = (HttpWebResponse)we.Response;
MessageBox.Show("Code d'erreur: "+r.StatusCode.ToString());
r.Close();
}
else
MessageBox.Show(we.Message + " " + we.Status.ToString());
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Erreur");
}
}

Comme vous le voyez, rien de particulier à signaler ici. Tout le code a déjà été vu précédemment.

Le résultat en image :

Conclusion

C'est ici que s'achève cet article consacré à la réalisation et l'utilisation d'un web service REST avec WCF 3.5. Ce dernier apporte un certain nombre de nouveautés rendant la création de service REST pratiquement un jeu d'enfant. Cependant, malgré son apparente facilité, WCF permet une personnalisation très avancée de vos services REST. Nous n'aborderons pas ce thème, mais le site MSDN vous permettra d'en savoir plus.

La création de clients consommant un service REST est elle aussi très facile (c'est d'ailleurs un des intérêts majeurs de REST). Savoir envoyer et lire des requêtes HTTP ainsi que manipuler du XML (s'il s'agit du format utilisé) sont les seuls prérequis nécessaires.

Liens

Introduction à Windows Communication Foundation Par Ronald VASSEUR

Introduction à Windows Communication Foundation Par Lainé Vincent

Representational State Transfer Chapitre 5 de la thèse de Roy Fielding

Wiki sur REST

Exemple de service REST avec WCF

La sérialisation WCF

REST-Based Ajax Services with WCF in .NET 3.5

Sources

Téléchargez les sources de la solution donnée en exemple.

Remerciements

J'adresse ici tous mes remerciements à l'équipe de rédaction de « developpez.com » (et plus particulièrement à Johann Blais) pour le temps qu'ils ont bien voulu passer à la correction et à l'amélioration de cet article.

Contact

Si vous constatez une erreur dans le tutoriel, dans les sources, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par le forum.