Introduction

L'Open Data Protocol est un nouveau standard suivant le style d'architecture REST dont le but est de permettre aux applications d'exposer des données en tant que service à travers des réseaux intranet ou le Web.

WCF Data Services quant à lui est un composant serveur du Framework .NET basé sur WCF permettant la création de services utilisant l'Open Data Protocol.

L'Open Data Protocol n'est pas spécifique à l'environnement .NET, n'importe quel client (Ajax, Java, PHP, etc.) sachant manipuler HTTP, XML, JSON pourra accéder à un service de ce type. Des librairies clientes dans différents langages sont déjà disponibles sur le site officiel.

De nombreux produits Microsoft (entre autres) supportent déjà ce protocole afin de partager ou consommer des données: SharePoint Server 2010, Excel 2010 (via SQL Server PowerPivot), Windows Azure Storage, SQL Server 2008 R2, Visual Studio 2008 SP1 et 2010.

Les spécifications de l'Open Data Protocol sont publiées sous licence Microsoft Open Specification Promise (OSP), ce qui permet à de tierces parties (dont des projets Open Source) de développer des services pour n'importe quelle plateforme et des clients pour les consommer.

Au travers de cet article nous découvrirons les bases de l'Open Data Protocol, ses relations avec Atom et AtomPub. Nous verrons comment utiliser WCF Data Services pour créer un service exposant les données d'une base et développerons un client WPF afin de consommer ce service.

I. Présentation d'OData

L'Open Data Protocol, plus communément appelé OData, est un protocole facilitant le partage, la création et la consommation de données au travers des réseaux d'entreprise et du web. OData est basé sur les technologies web telles que HTTP, Atom, Atom Publishing Protocol (AtomPub), JSON et embrasse le style d'architecture REST.

Vous trouverez plus d'informations sur le style d'architecture REST via l'article Introduction aux services web REST avec WCF 3.5.

Le site officiel d'OData (http://www.odata.org) recense un grand nombre de ressources et outils disponibles. Vous y trouverez des API clients et serveurs pour différents langages et plateformes (.NET, Java, PHP, Silverlight, Windows Phone 7, etc.), des exemples de code, des services OData disponibles à des fins de tests et de la documentation sur le protocole.

De plus, vous trouverez des ressources supplémentaires dans la section Liens à la fin de cet article.

I-A. Atom

Atom est un format de document basé sur XML décrivant une liste d'informations liées appelées « flux » (« feeds » en anglais). Un flux est composé de plusieurs « entrées » qui sont des ensembles extensibles de métadonnées. Pour chaque entrée vous retrouverez par exemple un titre, un auteur, etc. Atom est le format XML par défaut dans lequel les données OData sont publiées (il s'agit du format le plus couramment rencontré, mais vous pouvez aussi publier les données OData en JSON par exemple).

Voici un exemple (tiré des spécifications du format) de flux Atom contenant une entrée :

Exemple de flux Atom
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Example Feed</title>
  <link href="http://example.org/"/>
  <updated>2003-12-13T18:30:02Z</updated>
  <author>
    <name>John Doe</name>
  </author>
  <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
  <entry>
    <title>Atom-Powered Robots Run Amok</title>
    <link href="http://example.org/2003/12/13/atom03"/>
    <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
    <updated>2003-12-13T18:30:02Z</updated>
    <summary>Some text.</summary>
  </entry>
</feed>

En général, les flux Atom que vous rencontrez sur le Web (venant d'un blog ou d'un site) publient leurs données via les balises « title » (titre de l'entrée), « link » (lien vers l'entrée sur le blog ou le site) et « summary » (résumé du contenu). Il existe cependant une balise « content » permettant d'insérer le contenu de l'entrée. Le type de données contenu dans cette balise est spécifié via l'attribut « type ». Cela peut être « text », « html », « xhtml » ou un type MIME media (par exemple « application/xml »).

Voici un exemple avec un type « xhtml » :

Balise content
Sélectionnez
<content type="xhtml" xml:lang="en"
 xml:base="http://diveintomark.org/">
  <div xmlns="http://www.w3.org/1999/xhtml">
    <p>
      <i>[Update: The Atom draft is finished.]</i>
    </p>
  </div>
</content>

Une des forces d'Atom est son extensibilité. Il est ainsi possible d'insérer au sein d'une entrée des balises provenant de son propre vocabulaire après en avoir déclaré le schéma. L'élément « content » est justement désigné pour supporter l'inclusion arbitraire de balises étrangères. C'est justement sur cette extensibilité que s'appuie OData.

Analysons un exemple de flux OData :

Exemple de flux OData
Sélectionnez
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<feed xml:base="http://localhost:2666/AdventureWorksDataService.svc/" 
      xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" 
      xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" 
      xmlns="http://www.w3.org/2005/Atom">
  <title type="text">Categories</title>
  <id>http://localhost:2666/AdventureWorksDataService.svc/Categories</id>
  <updated>2010-04-05T11:03:15Z</updated>
  <link rel="self" title="Categories" href="Categories" />
  <entry>
    <id>http://localhost:2666/AdventureWorksDataService.svc/Categories(1)</id>
    <title type="text" />
    <updated>2010-04-05T11:03:15Z</updated>
    <author>
      <name />
    </author>
    <link rel="edit" title="Category" href="Categories(1)" />
    <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Products" 
          type="application/atom+xml;type=feed" 
          title="Products" 
          href="Categories(1)/Products" />
    <category term="AdventureWorksModel.Category" 
              scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
    <content type="application/xml">
      <m:properties>
        <d:CategoryID m:type="Edm.Int32">1</d:CategoryID>
        <d:ParentCategoryID m:type="Edm.Int32" m:null="true" />
        <d:Name>Bikes</d:Name>
        <d:rowguid m:type="Edm.Guid">cfbda25c-df71-47a7-b81b-64ee161aa37c</d:rowguid>
        <d:ModifiedDate m:type="Edm.DateTime">1998-06-01T00:00:00</d:ModifiedDate>
      </m:properties>
    </content>
  </entry>
</feed>

Les schémas propres à OData ont été déclarés au sein de la balise « feed ». Le type MIME « application/xml » est utilisé pour spécifier le type de contenu de la balise « content ». Au sein de cette balise nous utilisons des balises propres à OData.

Cela reste un flux Atom tout à fait valide (vous pouvez par exemple le lire via un lecteur de flux Atom) mais il est maintenant étendu afin de contenir des donnés OData.

I-B. AtomPub

Atom Publishing Protocol (AtomPub) est un protocole permettant la publication et l'édition de ressources Web en utilisant HTTP et XML.

Les trois concepts principaux d'AtomPub sont :

  • « Collections » : un ensemble éditable de ressources représentées par un flux Atom.
  • « Services » : ils permettent la découverte et la description des « Collections ».
  • Édition : il est possible de créer, d'éditer et de supprimer des ressources en utilisant des requêtes HTTP et les verbes GET, POST, PUT et DELETE.

Un « Service Document » regroupe des « Collections » au sein de Workspaces. C'est le point d'entrée pour tout client désirant connaitre les collections disponibles.

Voici un exemple simple de « Service Document » contenant un « Workspace » :

Service Document
Sélectionnez
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<service xml:base="http://localhost:2666/AdventureWorksDataService.svc/" 
         xmlns:atom="http://www.w3.org/2005/Atom" 
         xmlns:app="http://www.w3.org/2007/app" 
         xmlns="http://www.w3.org/2007/app">
  <workspace>
    <atom:title>Default</atom:title>
    <collection href="Products">
      <atom:title>Products</atom:title>
    </collection>
    <collection href="Categories">
      <atom:title>Categories</atom:title>
    </collection>
    <collection href="Models">
      <atom:title>Models</atom:title>
    </collection>
  </workspace>
</service>

Ce « Workspace » contient trois « Collections » : Products, Categories et Models. L'attribut href spécifie l'adresse (relative) de cette collection.

Pour résumer : « Service Document » --> « Workspaces » --> « Collections » --> « Atom feed » --> « Atom entry »

AtomPub permet à un client de manipuler les entrées de « Collections » grâce aux quatre verbes HTTP:

  • GET : obtient une représentation d'une entrée existante
  • POST : crée une nouvelle entrée
  • PUT : modifie une entrée existante
  • DELETE : supprime une entrée

I-C. OData

I-C-1. Métadonnées d'un service OData

Un service OData expose deux types de métadonnées. Le premier est un « Service Document » relatif au protocole AtomPub que nous avons vu précédemment et qui présente les collections de ressources disponibles. Le deuxième est un document (XML) de métadonnées du service. Ce document décrit le model de donnée exposé en HTTP par le service : la structure des ressources, les liens entre ces ressources, les opérations de service. Le contenu de ce document utilise des termes Entity Data Model (EDM) qui est un langage XML de description de modèles appelé ConceptualSchemaDefinitionLanguage (CSDL). L'accès à ce document se fait en rajoutant « $medatata » à l'URL root d'un service OData. Par exemple : http://monsite.com/OData.svc/$metadata .

Voici un exemple de document de métadonnées d'un service OData :

metadata.png

Ce document décrit trois entités exposées par le service : Product, Category et Model et trois relations les liant.


Pour chaque entité, le document décrit ses différentes composantes (propriétés, associations, etc.) :

metatdata-category.png

Ce document est à un service OData ce qu'un document WSDL est à un Web Service SOAP. Il va faciliter la consommation du service par les clients en permettant aux outils de génération de code d'en extraire les métadonnées afin de générer des proxys.

I-C-2. URI

OData s'appuie sur le style d'architecture REST dont un point fondamental est la notion de ressource. Chaque ressource est adressable via une URI (Uniform Resource Identifiers).

OData définit un certain nombre de règles pour la construction d'URI afin d'identifier des ressources et des métadonnées exposées par un service, ainsi qu'un ensemble d'opérateurs (passés en paramètre d'URI) permettant d'effectuer des requêtes sur celles-ci.

Les URI des services OData se découpent généralement en trois parties : l'URI root du service, un chemin d'accès aux ressources et des paramètres de requête.

Le schéma suivant présente un exemple d'URI et ses trois composantes :

uri-odata

I-C-2-1. URI root du service

La ressource associée à l'URI root du service est un « Service Document » du protocole AtomPub (que nous avons vu précédemment). Il recense les collections de ressources exposées par le service.

I-C-2-2. Chemin de la ressource

Le chemin de la ressource identifie celle avec laquelle interagir. Il permet l'accès à n'importe quel élément du modèle de données exposé par le service OData (collection d'entrées, entrée unique, propriétés, liens, opération de service, etc.).

Par exemple, l'accès à l'ensemble des entrées « Category » se fera en incluant le nom de la collection à l'URI :
http://localhost:2666/AdventureWorksDataService.svc/Categories.

Après le nom d'une collection d'entrées il est possible de spécifier entre parenthèses un filtre optionnel permettant de restreindre la réponse à une seule entrée. Dans le cas d'une entrée ayant une clef composée d'une seule propriété il est possible de n'indiquer que la valeur dans le filtre à la place du couple « nom de la propriété/valeur ».

Ainsi, dans le cas d'une entité « Category » dont l'identifiant est la propriété CategorieID, les deux URI suivantes sont équivalentes pour adresser l'entrée d'identifiant 18 de la collection « Categories » :
http://localhost:2666/AdventureWorksDataService.svc/Categories(18)
http://localhost:2666/AdventureWorksDataService.svc/Categories(CategoryID=18)

Vous pouvez ensuite ajouter le nom d'une propriété de navigation afin de, par exemple, accéder aux entités « Product » associées à une entité « Category » :
http://localhost:2666/AdventureWorksDataService.svc/Categories(18)/Products

La propriété « Products » étant elle-même une collection, vous pouvez continuer à construire l'URI en rajoutant un filtre pour accéder à un seul produit, puis rajouter une propriété de navigation, etc.

Les associations (liens) entre les entrées sont adressables de la même manière que n'importe quelle autre ressource. La règle de base pour adresser les relations est montrée dans le schéma suivant :

uri-association-odata.png


Par exemple, voici l'URI pour récupérer la liste des liens vers les produits associés à la catégorie d'identifiant 19 :
http://localhost:2666/AdventureWorksDataService.svc/Categories(19)/$links/Products

Et le résultat retourné :

 
Sélectionnez
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<links xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices">
  <uri>http://localhost:2666/AdventureWorksDataService.svc/Products(908)</uri>
  <uri>http://localhost:2666/AdventureWorksDataService.svc/Products(909)</uri>
  <uri>http://localhost:2666/AdventureWorksDataService.svc/Products(910)</uri>
  <uri>http://localhost:2666/AdventureWorksDataService.svc/Products(911)</uri>
  <uri>http://localhost:2666/AdventureWorksDataService.svc/Products(912)</uri>
  <uri>http://localhost:2666/AdventureWorksDataService.svc/Products(913)</uri>
  <uri>http://localhost:2666/AdventureWorksDataService.svc/Products(914)</uri>
  <uri>http://localhost:2666/AdventureWorksDataService.svc/Products(915)</uri>
  <uri>http://localhost:2666/AdventureWorksDataService.svc/Products(916)</uri>
</links>

I-C-2-3. Paramètres de requête

La section des paramètres de requête d'un URI OData permet de spécifier trois types d'informations : des options de requête système, des options de requête personnalisées et des paramètres d'opérations de service.

Les options de requête système sont des paramètres qu'un client peut spécifier afin de contrôler la quantité et l'ordre des données qu'un service OData retourne pour une ressource identifiée par un URI. Le nom de chaque paramètre est préfixé par le caractère « $ ».

Option Description
$orderby Spécifie une expression pour déterminer la ou les propriétés qui sont utilisées pour ordonner la collection d'entrées identifiée par la section « chemin de la ressource » de l'URI. Les mots « asc » et « desc » permettent de spécifier si l'ordre est ascendant ou descendant.
Exemple: /Categories?$orderby=Name desc
$top Permet de restreindre le nombre d'entrées retournées. Utilisé avec l'option $skip, il permet d'implémenter de la pagination.
Exemple :/Products?$top=5
$skip Permet de sauter un nombre donné d'entrées du résultat. L'option $skip s'utilise sur des liste préalablement ordonnées. Si aucun ordre n'est spécifié (via l'option $orderby), les entrées seront ordonnées par leur identifiant avant l'application de l'option $skip.
Exemple :/Categories?$skip=10&$top=5
$filter Permet d'appliquer une expression de filtre sur une collection d'entrée.
Exemple :/Products?$filter=Color eq 'Black'
$expand Permet d'inclure un ensemble d'entités relatives au résultat retourné. Cette option est similaire à l'instruction « Include » utilisée dans Entity Framework.Vous pouvez ainsi par exemple requêter une liste de produits et inclure les catégories associées au sein d'une même requête HTTP.
Exemple:/Products(680)?$expand=Category
$format Le format de retour désiré (Atom, JSON, etc.) doit en principe être spécifié dans l'en-tête HTTP « Accept » de la requête HTTP envoyée par le client. Mais il est possible de le spécifier dans l'URI via l'option $format. Le format spécifié dans l'URI prédomine sur celui spécifié dans l'en-tête.
Exemple :/Products?$format=json
$select Permet de spécifier un sous-ensemble de propriétés d'une entrée à retourner. Les propriétés sont séparées par une virgule.
Exemple :/Products(680)?$select=Name,Color
$inlinecount Permet de retourner le nombre total d'entrées au sein de la collection requêtées. Ce nombre est calculé après application d'un éventuel filtre.
Exemple :/Products?$filter=Color eq 'Black'&$top=5&$inlinecount=allpages
Il y a 89 produits de couleur noire en base. L'URI précédente ne retournera que les cinq premiers. Mais l'option $inlinecount permet d'inclure dans le résultat le fait qu'il y en a 89.


Les options de requête personnalisées, quant à elles, sont un moyen d'étendre un service OData. Elles sont représentées par un ensemble de paires clef/valeur à la fin de l'URI. Le nom de la clef ne commence pas par le caractère « $ ».

Par exemple, l'URI suivant permet d'accéder à l'ensemble des ressources de la collection « Categories » :
http://localhost:2666/AdventureWorksDataService.svc/Categories?p=toto

Une option de requête personnalisée « p » est aussi spécifiée. La signification de cette option est propre au service.

Les paramètres d'opérations de service représentent les paramètres d'une opération de service exposée par OData. Ces paramètres sont passés à la fin d'une URI sous la forme de paires clef/valeur.

Par exemple l'URI suivante permet d'appeler l'opération de service « GetProductsByColor » en lui passant la valeur « red » pour le paramètre « color » :
http://localhost:2666/AdventureWorksDataService.svc/GetProductsByColor?color=red

Vous retrouverez plus d'informations sur les opérateurs disponibles au sein des spécifications du format OData.

I-C-3. Tester simplement un service OData

La façon la plus simple de tester un service OData est de passer un navigateur. Vous pourrez saisir une URI à requêter dans la barre d'adresse et visualiser le résultat retourné.

Le résultat d'une requête sur un service OData renvoyant par défaut un flux Atom, il faut désactiver dans le navigateur la vue utilisée pour le rendu des flux Atom/RSS afin de visualiser le XML retourné. Dans Internet Explorer cela se fait via le menu Outils/Option Internet/Contenu :

Voici par exemple le résultat retourné pour l'URI http://localhost:2666/AdventureWorksDataService.svc/Categories(1) sur un service exposant les données de la base AdventureWorks :

odata-ie.png

Bien que faciles d'utilisation, les navigateurs ne vous permettent pas d'effectuer des requêtes HTTP utilisant les verbes PUT ou DELETE, de modifier les en-têtes http ou encore de visualiser des données au format JSON.

Pour pallier ces manques, vous pouvez utiliser le logiciel Fiddler.

L'onglet RequestBuilder de Fiddler vous permet de construire des requêtes HTTP en spécifiant, par exemple, le verbe HTTP à utiliser, les valeurs de l'en-tête, le contenu du corps du message, etc.

Par exemple, voici une requête HTTP GET sur l'URI http://127.0.0.1.:2666/AdventureWorksDataService.svc/Categories(1). Nous avons ici spécifié dans l'en-tête « accept » le type MIME application/json qui indique au service OData que l'on souhaite récupérer le résultat au format JSON.

fiddler-json.png
Demande d'une ressource au format JSON


En analysant le résultat on voit que celui-ci a bien été renvoyé au format JSON :

fiddler-json-response.png
Retour au format JSON

I-C-4. Création d'une entrée

La création d'une nouvelle entrée au sein d'une collection se fait via une requête HTTP POST sur l'URI de la collection.

Si l'on souhaite par exemple ajouter une nouvelle catégorie à la collection « Categories », il faut poster à l'URI http://127.0.0.1.:2666/AdventureWorksDataService.svc/Categories. Le corps de la requête doit contenir la nouvelle entrée dans un format accepté par le service (Atom ou JSON). L'en-tête « content-type » indique le format de l'entrée contenue dans le corps du message (ici Atom). L'en-tête « accept » indique le format dans lequel on souhaite obtenir la réponse du service.

fiddler-post.png
Ajout d'une ressource via HTTP POST


Le service crée la nouvelle ressource et renvoie au client une réponse contenant la nouvelle ressource (dans le format spécifié via l'en-tête « accept » de la requête) ainsi que l'URI de cette nouvelle ressource via l'en-tête « location ».

fiddler-post-response.png
Réponse d'une requête POST

I-C-5. Modification d'une entrée

La mise à jour d'une entrée peut s'effectuer de deux manières : via une opération de fusion ou via une opération de remplacement.

Le protocole AtomPub définit la mise à jour via une requête HTTP PUT envoyée à l'URI de la ressource à modifier. Une requête HTTP PUT modifie une ressource en la remplaçant par celle spécifiée dans le corps de la requête. Le corps de la requête doit donc contenir une ressource complète avec toutes ses propriétés. Il n'est cependant pas obligatoire de spécifier les valeurs associées à ces propriétés ; si elles sont manquantes celles-ci seront remplacées par leur valeur par défaut.

Dans certains cas il est intéressant de modifier une ressource, non pas en la remplaçant, mais en la fusionnant avec une ressource envoyée par un client. Celle-ci n'a pas besoin d'être complète (contrairement à une requête PUT). Seules les propriétés présentent dans la requête seront mises à jour côté service. Il n'existe pas de verbe HTTP correspondant à ce type de mise à jour et le protocole AtomPub ne le prend pas en charge. Ainsi, plutôt que de surcharger le verbe PUT, OData introduit un nouveau verbe : MERGE.


Voici un exemple d'utilisation où l'on met à jour une catégorie en ne spécifiant que son nouveau nom :

fiddler-merge.png
Requête HTTP Merge

Il est question d'introduire le verbe PATCH au sein du protocole HTTP. Une fois finalisé, celui-ci remplacerait alors le verbe MERGE au sein d'OData afin de respecter le protocole.

Il est aussi possible de mettre à jour les propriétés d'une ressource de façon individuelles via une requête HTTP PUT sur l'URI de cette propriété. Le corps de la requête ne contient alors que la propriété et sa nouvelle valeur.

fiddler-put-propertie.png
Mise à jour d'une propriété

I-C-6. Suppression d'une entrée

La suppression d'une entrée se fait en envoyant une requête HTTP DELETE sur l'URI de l'entrée en question. Dans le cas où celle-ci est liée à d'autres entrées, il revient au service de décider si la suppression s'effectue en cascade ou non.

I-C-7. Manipulation des associations

Les associations entre entrées sont elles aussi modifiables. Il est possible de modifier un lien de deux manières : en modifiant une entrée ou en modifiant directement le lien.

Voici un exemple où l'on modifie la catégorie d'un produit directement via l'URI de l'association. L'association entre le produit d'identifiant 680 et sa catégorie est adressable via l'URI /Products(680)/$links/Category.

Une requête HTTP GET sur l'URI de l'association indique que le produit est lié à la catégorie d'identifiant 18 :

fiddler-links-get.png

Pour associer dorénavant ce produit à la catégorie d'identifiant 1, il suffit d'envoyer une requête HTTP PUT contenant la nouvelle valeur pour cette association sur l'URI de l'association.

image

Si l'on renvoie une requête HTTP GET sur l'URI de l'association, nous obtenons le résultat suivant, indiquant que le produit a bien changé de catégorie :

fiddler-links-get2.png

La création et la suppression d'associations s'effectuent de la même manière via des requêtes HTTP POST et DELETE.

II. Création d'un service OData en .NET

WCF DATA Services est la technologie .NET permettant la création et la consommation de Web Service exposant des données via le protocole OData.

Voici un diagramme tiré de la documentation MSDN illustrant l'architecture utilisée pour exposer et consommer des flux OData :

architecture.bmp


WCF Data Services est basé sur la technologie WCF et comme tout service WCF nous aurons besoin d'un hôte pour l'hébergement du service. Vous pouvez héberger le service au sein d'une application ASP.NET (via IIS) ou dans n'importe quelle application .NET (console, service Windows, etc.) via l'auto-hébergement.

Dans notre exemple nous utiliserons le template WCF Service Application et ciblerons le Framework 4.0.

new-wcf-project.png

Par défaut ce template génère un Web Service « Service1 » ; vous pouvez supprimer tout ce qui a trait à ce service.

II-A. Création du model de données

WCF DATA Services expose des données indépendamment de leur origine (base de données, fichiers, objet en mémoire) grâce à un système de provider (voir schéma d'architecture plus haut). Concernant les données issues d'une base de données relationnelle, tout est fait pour que le plus simple à utiliser soit un modèle Entity Framework. C'est donc sur un modèle de ce type que nous construirons notre service.

Concernant les données nous utiliserons la base SQL Server AdventureWorks (dans sa version light) disponible sur le site Codeplex. Il s'agit d'une base d'exemple représentant une compagnie fictive de vente de vélos.

Ajoutez un nouvel élément de type ADO.NET Entity Data Model au projet et nommez-le AdventureWorksModel.edmx.

new-edmx.png

Créez une connexion vers la base AdventureWorks :

new-edmx-2.png

Puis sélectionnez les tables Product, ProductCategory et ProductModel :

new-edmx-3.png

Vous devriez obtenir un model proche de celui-ci (après avoir renommé quelques propriétés) :

EntityDesignerDiagram.png

II-B. Création d'un projet WCF Data Services

Nous allons maintenant exposer le model de données via un service OData.

Ajoutez un nouvel élément au projet en sélectionnant le template WCF Data Services. Appelez-le AdventureWorksDataService.svc.

new-wcf-data-service.png

La solution doit maintenant ressembler à ceci :

solution-odata.png

Le fichier AdventureWorksDataService.svc contient les instructions pour le runtime WCF afin qu'il instancie le service OData via un objet de type DataServiceHostFactory.

Contenu du fichier AdventureWorksDataService.svc
Sélectionnez
<%@ ServiceHost Language="C#" Factory="System.Data.Services.DataServiceHostFactory, 
    System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" 
    Service="DemoOData.AdventureWorksDataService" %>


Dans le fichier de code associé nous retrouvons l'implémentation du service de données :

Classe AdventureWorksDataService
Sélectionnez
public class AdventureWorksDataService : DataService<AdventureWorksEntities>
{
    // This method is called only once to initialize service-wide policies.
    public static void InitializeService(DataServiceConfiguration config)
    {
        config.SetEntitySetAccessRule("*", EntitySetRights.All);
        config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
    }
        
}

II-B-1. Configuration du service

Un service de données est une classe héritant de la classe générique DataService dont le type est celui de la source de données (ici notre model Entity Framework).

La méthode InitializeService vous permet de configurer le service. Cette méthode fournit en paramètre un objet de type DataServiceConfiguration contenant les paramètres de configuration du service.

II-B-1-1. Droits d'accès

La méthode SetEntitySetAccessRule permet de spécifier les droits d'accès pour chaque collection de ressources. Le premier paramètre est le nom de la collection, le deuxième représente les droits accordés.

Par exemple :

 
Sélectionnez
config.SetEntitySetAccessRule("Products", EntitySetRights.All);
config.SetEntitySetAccessRule("Categories", EntitySetRights.AllRead);
config.SetEntitySetAccessRule("Models", EntitySetRights.None);

Dans l'exemple précédant les clients ont tous les droits (lecture, modification, suppression) sur la collection des produits, uniquement le droit de lecture sur la collection des catégories et aucun sur la collection des modèles. Le caractère « * » à la place du nom d'une ressource signifie « l'ensemble des collections du modèle de données ».

II-B-1-2. Pagination

La méthode SetEntitySetPageSize permet d'activer la pagination en limitant le nombre de résultats retournés par une requête cliente.

L'exemple suivant limite à 10 le nombre de produits pouvant être retournés par n'importe quelle requête cliente :

 
Sélectionnez
config.SetEntitySetPageSize("Products", 10);

Si un client effectue une requête retournant plus de 10 produits alors le service renverra les 10 premiers produits ainsi qu'un lien vers les 10 suivants.

Par exemple, une requête sur l'URI http://localhost:2666/AdventureWorksDataService.svc/Products renverra le résultat suivant:

image

Seul dix entrées sont retournées ainsi qu'un lien permettant d'obtenir les dix suivantes.

Il existe de nombreux autres paramètres de configuration pour lesquels vous trouverez plus d'information sur la MSDN.

II-B-2. Intercepteurs

WCF Data Services vous permet de définir des intercepteurs au niveau des requêtes effectuées sur le service. Vous pouvez, par exemple, valider des paramètres d'entrée, modifier les requête d'origine, définir des restrictions d'accès, définir des règles métier, etc.

Les intercepteurs agissent au niveau des entités exposées par le service. Il existe deux types d'intercepteurs : les « queryinterceptors » qui agissent sur les requêtes HTTP GET (lecture de données) et les « change interceptors » qui agissent sur les requêtes modifiants des données.

Les « queryinterceptors » possèdent l'attribut QueryInterceptor et retournent une expression lambda permettant d'évaluer si une entité peut faire partie du résultat renvoyé par la requête cliente.

Voici un exemple de « queryinterceptor » pour la collection des catégories :

Query Interceptor
Sélectionnez
// On définit un intercepteur pour la collection Categories.
[QueryInterceptor("Categories")]
public Expression<Func<Category, bool>> OnQueryCategories()
{
    // on ne renvoie une catégorie que si elle a au moins un produit attaché
    return c => c.Products.Any();
}

Seules les catégories ayant au moins un produit associé seront exposées par le service et renvoyées suite à des requêtes HTTP GET.

Si l'on tente d'accéder directement via son URI à une catégorie ne possédant pas de produit associé (par exemple /AdventureWorksDataService.svc/Categories(1)) nous obtenons une erreur 404 Not Found ainsi qu'un message d'erreur :

image

Les « change interceptors » sont des méthodes avec l'attribut ChangeInterceptor ne retournant rien et prenant deux paramètres. Le premier correspond à l'entité qui va être modifiée (sa valeur correspond aux informations envoyées par la requête HTTP). Le deuxième est une énumération de type UpdateOperation et indique le type d'opération en cours sur l'entité (ajout, modification, suppression).

Voici un exemple de « change interceptor » agissant sur la collections des produits :

Change Interceptor
Sélectionnez
// On définit un change interceptor pour la collection Products.
[ChangeInterceptor("Products")]
public void OnChangeProducts(Product product, UpdateOperations operations)
{
    if (operations == UpdateOperations.Change)
    {
        // On rejette les changements sur les produits abandonnés
        using (AdventureWorksEntities ctx = new AdventureWorksEntities())
        {
            Product originalProduct = ctx.Products.Where(p => p.ProductID == product.ProductID).First();

            // On rejette les changements sur les produits abandonnés
            if (originalProduct.DiscontinuedDate != null)
            {
                throw new DataServiceException(400,
                            "A discontinued product cannot be modified");
            }
        }
    }
    else if (operations == UpdateOperations.Delete)
    {
        // On rejette la suppression. On ne peut pas supprimer un produit, seulement lui mettre une date d'arrêt de commercialisation.
        throw new DataServiceException(400,
            "Products cannot be deleted; instead set the DiscontinuedDate property");
    }
}

Ce « change interceptor » permet de mettre en place deux règles métier en interceptant toutes les requêtes clientes agissant sur les produits : la non modification d'un produit dont la commercialisation a été abandonnée (propriété DiscontinuedDate non null) et le fait de ne pas pouvoir supprimer un produit (seulement le marquer comme étant abandonné via la propriété DiscontinuedDate).

Ainsi, une requête HTTP DELETE sur l'URI /AdventureWorksDataService.svc/Products(680) renverra une erreur 400 Bad Request ainsi que le message d'erreur défini dans l'intercepteur :

fiddler-delete-error.png

II-B-3. Opérations de service

Les opérations de service sont des méthodes que vous pouvez exposer via un service WCF Data Services. Elles sont adressables via une URI comme n'importe quelle ressource du service.

Les opérations de service sont annotées par l'attribut [WebGet] pour celles adressables via une requête HTTP GET et par l'attribut [WebInvoke] pour celles adressables via une requête HTTP POST.

Des paramètres de type simple uniquement peuvent être spécifiés pour les opérations de service. Celles-ci peuvent retourner void (Nothing en VB.NET), IEnumerable, IQueryable, une entité du modèle de donnée ou un type primitif. Retourner IQueryable permet de supporter des options de requête (pagination, filtre, etc.) et de naviguer via les propriétés de navigation des entités retournées.

L'exemple d'opération de service ci-dessous permet d'obtenir un ensemble de cinq produits recommandés en fonction du type de personne passé en paramètre (fille ou garçon) :

Opération de service
Sélectionnez
// Opération de service
[WebGet]
public IQueryable<Product> GetRecommendedProductsByGender(string gender)
{
    if (string.IsNullOrEmpty(gender))
    {
        throw new ArgumentNullException("gender",
            "You must provide a value for the parameter'gender'.");
    }

    if (gender != "boy" && gender != "girl")
    {
        throw new ArgumentException("Bad value for the parameter 'gender'.", "gender");
    }

    try
    {
        string color = gender == "boy" ? "black" : "red";
        var products = from product in this.CurrentDataSource.Products
                                where product.Color == color
                                && product.DiscontinuedDate == null
                                orderby product.ListPrice descending
                                select product;

        return products.Take(5);
    }
    catch (Exception ex)
    {
        throw new ApplicationException("An error occured: {0}", ex);
    }
}

La propriété CurrentDataSource représente la source de données associée au service (dans notre cas, le modèle Entity Framework).

La visibilité des opérations de service est contrôlée lors de l'initialisation du service (méthode InitializeService) via la méthode SetServiceOperationAccessRule:

 
Sélectionnez
config.SetServiceOperationAccessRule("GetRecommendedProductsByGender", ServiceOperationRights.AllRead);


Par exemple, une requête HTTP GET dans un navigateur sur l'URI /AdventureWorksDataService.svc/GetRecommendedProductsByGender?gender='boy' renverra le résultat suivant :

GetRecommendedProductsByGender.png

II-B-4. Gestion du cache

L'infrastructure WCF Data Services étant basée sur http et OData étant un ensemble de conventions au-dessus d'HTTP, il est possible d'utiliser les possibilités offertes par HTTP pour améliorer le service, notamment en ce qui concerne la mise en cache.

La mise en cache de données côté serveur et côté client permet d'améliorer drastiquement les performances d'un service basé sur HTTP.

La classe représentant le service (DataService) possède une méthode OnStartProcessingRequest qui est appelée avant le traitement de chaque requête. Il est possible de redéfinir cette méthode afin d'y ajouter des actions supplémentaires.

Voici un exemple de mise en place d'un cache sur la collection Categories :

Mise en place du cache
Sélectionnez
// Appelée avant chaque requête
protected override void OnStartProcessingRequest(ProcessRequestArgs args)
{
    base.OnStartProcessingRequest(args);

    string[] segments = args.RequestUri.Segments;
    bool isQueryForCategory = segments[segments.Length - 1].Contains("Categories");

    string method = HttpContext.Current.Request.HttpMethod.ToUpper();

    // cache sur toutes les entités catégories
    if (method == "GET" && isQueryForCategory)
    {
        HttpCachePolicy c = HttpContext.Current.Response.Cache;
        c.SetCacheability(HttpCacheability.ServerAndPrivate);
        c.SetExpires(HttpContext.Current.Timestamp.AddSeconds(60));
        c.VaryByHeaders["Accept"] = true;
        c.VaryByHeaders["Accept-Charset"] = true;
        c.VaryByHeaders["Accept-Encoding"] = true;
        c.VaryByParams["*"] = true;
    }
}

On vérifie en premier si la requête concerne la collection Categories en inspectant l'URI adressé et s'il s'agit d'une requête HTTP GET.

La classe HttpCachePolicy permet de définir des en-têtes HTTP propres au cache.

La méthode SetCacheability permet de contrôler la valeur de l'en-tête Cache-Control. La valeur ServerAndPrivate de l'énumération HttpCacheability indique que la réponse est mise en cache côté serveur et client mais pas au niveau des serveurs proxys.

La méthode SetExpires affecte une date et une heure absolues à l'en-tête HTTP Expires. On indique ici une durée de 60 secondes.

La propriété VaryByHeaders permet de spécifier les en-têtes qui feront varier la sortie du cache. Ainsi, pour une même URI, une version séparée de la réponse sera disponible dans le cache pour chaque type d'en-tête http spécifié.

La propriété VaryByParams spécifie les paramètres d'une requête HTTP GET ou POST qui affecterons la mise en cache. Le caractère « * » indique que l'ensemble des paramètres affecterons le cache. Ainsi, pour deux requêtes pointant vers le même URI mais avec des paramètres différents il en résultera deux versions séparées de la réponse dans le cache.

Lors d'une requête HTTP GET sur la liste des catégories, le client recevra l'en-tête HTTP suivants :

fiddler-cache.png

II-B-5. Gestion des erreurs

Lorsqu'une erreur survient au niveau du service il convient d'un informer le client. Pour cela WCF Data Services fournit le type DataServiceException qui permet de lever une exception comprise par le service. Ce type permet de contrôler les informations envoyées au client.

Afin de gérer les différents types d'exceptions (ArgumentException, InvalidOperationException, exception personnalisées, etc.) pouvant être lancés par les composants du service et qui ne sont donc pas du type DataServiceException, WCF Data Services propose la méthode HandleException.

Celle-ci est appelée à chaque fois qu'une exception non gérée survient dans le service. Il est ainsi possible d'intercepter toute exception qui ne serait pas de type DataServiceException et de lancer une exception DataServiceException correspondante à la place.

 
Sélectionnez
protected override void HandleException(HandleExceptionArgs args)
{
    if (args.Exception.InnerException is ArgumentException)
    {
        ArgumentException e = (ArgumentException)
                    args.Exception.InnerException;
        args.Exception = new DataServiceException(400,
                "ErreurPropriété:" + e.ParamName,
                "Description du problème",
                "fr-FR",
                    e);
    }
    else if (args.Exception.InnerException is CustomException)
    {
        throw new DataServiceException(400, args.Exception.Message);
    }
}


Voici un exemple d'erreur au format XML que peut renvoyer un service OData:

Erreur renvoyée par le service
Sélectionnez
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
  <code>ErreurPropriété:gender</code>
  <message xml:lang="fr-FR">Description du problème</message>
</error>

II-C. Partager son service

OData a pour le but le partage de données. Mais dans le cadre d'un service disponible publiquement (en complément d'un site Web par exemple), comment informer l'utilisateur de la disponibilité d'un tel service ?

Vous pouvez par exemple afficher l'icône OData sur la page Web avec un lien pointant vers le service (comme l'on ferait pour un flux RSS ou Atom). En voici un exemple sur le site nerddinner :

image

Cependant, les navigateurs modernes peuvent par exemple détecter la présence d'un flux RSS ou Atom grâce à des balises spéciales insérées dans la page Web et en informer l'utilisateur. Ne serait-il pas intéressant d'avoir la même chose pour un service OData (d'autant plus qu'Atom en constitue l'ossature) ?

Et bien, ces balises existent ! En voici deux exemples (repris du site nerddinner) :

Balises OData dans une page Web
Sélectionnez
<link rel="odata.service" title="NerdDinner.com OData Service" href="/Services/OData.svc" />

<link rel="odata.feed" title="NerdDinner.com OData Service - Dinners" href="/Services/OData.svc/Dinners" />

Le premier lien pointe vers le service tandis que le second pointe vers la collection "Dinners".

Malheureusement, OData étant assez jeune, les navigateurs actuellement disponibles sur le marché ne sont pas capables d'interpréter ces balises. Leurs prochaines versions le permettront peut-être.

Si vous disposez de gros volume de données et souhaitez les mettre à disposition (gratuitement ou non), sans doute serez-vous intéressé par Microsoft Dallas. Il s'agit pour résumer d'un MarketPlace (équivalent Microsoft à l'App Store d'Apple) pour données reposant sur Windows Azure. Les fournisseurs de données peuvent mettre à disposition leurs données qui peuvent ensuite être consommées notamment en OData. De gros organismes y sont déjà présents : Gouvernement Américain, UNESCO, NASA, National Geographic.

III. Création d'un client OData en .NET

III-A-1. Présentation de l'application

Nous allons créer une application WPF avec l'interface graphique suivante :

appli-wpf.png

Une liste déroulante permettra de sélectionner une catégorie. La liste des produits associés sera affichée en dessous. Comme nous avions mis en place de la pagination côté service sur la liste des produits (dix produits par page), un bouton en bas à droite permettra de charger ceux-ci page par page.

Un bouton « supprimer » tentera de supprimer le produit sélectionné. Cette opération devrait échouer étant donné que le service possède un « change interceptors » interdisant la suppression d'un produit.

Le bouton « abandonner » supprimera fonctionnellement un produit en mettant à jour sa date de fin de commercialisation.

Enfin, le bouton « ajouter » permettra d'ajouter un nouveau produit à la catégorie sélectionnée.

III-A-2. Ajout d'une référence au service OData

L'ajout d'une référence au service WCF Data Services se fait de la même façon que pour n'importe quel Web Service. Grâce au fichier de métadonnées (que nous avons vu précédemment) Visual Studio va générer un proxy (contenant une classe pour chaque entité du service ainsi qu'une classe dérivant de DataServiceContext et représentant le service en lui-même) permettant d'interagir plus facilement avec le service. Ce proxy est généré via l'utilitaire DataSvcUtil.exe.

add-service-reference.png

Le proxy s'instancie en passant en paramètre un URI vers le service WCF Data Services :

Création du proxy
Sélectionnez
AdventureWorksEntities ctx;

public MainWindow()
{
    InitializeComponent();

    ctx = new AdventureWorksEntities(new Uri("http://localhost:2666/AdventureWorksDataService.svc"));
    ctx.MergeOption = MergeOption.AppendOnly;
}

La propriété MergeOption contrôle les options de synchronisation lors de la réception ou l'envoi de données du service. La valeur AppendOnly est la valeur par défaut et indique que seules les nouvelles entités n'existant pas côté client seront rattachées au contexte. Les entités déjà chargées côté client ne seront pas mises à jour si des modifications ont été effectuées côté service et rapatriées côté client.

Bien qu'il soit possible de requêter le service en spécifiant une URI via la méthode CreateQuery du contexte, il est souvent plus pratique d'utiliser la puissance de LINQ pour effectuer ce travail. La librairie cliente se chargera de traduire les requêtes LINQ en URI pour vous.

III-A-3. L'Open Data Protocol Visualizer

L'Open Data Protocol Visualizer est une extension à Visual Studio 2010 qui permet d'obtenir une vue graphique des différents types et relations contenus dans un service WCF Data Services.

Cette extension peut s'installer via le gestionnaire des extensions de Visual Studio 2010 (menu Outils).

ODataVisualizer.png

Une fois installé, vous pouvez générer le diagramme via un clic droit sur le proxy créé lors de l'ajout de la référence vers le service:

image


Voici le diagramme généré dans le cas du service nous servant d'exemple :

image

III-A-4. Chargement de la liste des catégories

Voyons comment charger les catégories dans la liste déroulante (liée à une CollectionViewSource côté XAML).

La requête au service se fera en asynchrone afin de ne pas bloquer l'interface graphique.

Récupération de la liste des catégories
Sélectionnez
private void Window_Loaded(object sender, RoutedEventArgs e)
{
    CollectionViewSource categoriesViewSource = 
        ((CollectionViewSource)(this.FindResource("categoriesViewSource")));

    var categories = from c in ctx.Categories
                        orderby c.Name
                        select c;

    ((DataServiceQuery<Category>)categories)
        .BeginExecute(delegate(IAsyncResult ar)
            {
                var cat = ((DataServiceQuery<Category>)categories).EndExecute(ar);
                Dispatcher.Invoke((Action)
                    delegate { categoriesViewSource.Source = cat; });
            },
            null);
}

Nous créons une requête LINQ afin de récupérer les catégories ordonnées par leur nom. Cette requête peut être « castée » en objet DataServiceQuery qui représente une requête sur un service WCF Data Services. Cela nous permet d'avoir accès à la méthode BeginExecute afin d'exécuter la requête en asynchrone. Il s'agit ensuite du pattern classique des méthodes asynchrones. Nous déclarons un délégué avec une méthode anonyme dans la laquelle nous appelons la méthode EndExecute afin de récupérer la liste des catégories.

Si nous mettons un point d'arrêt au niveau de l'exécution de la requête nous pouvons visualiser l'URI générée par LINQ et correspondant à la requête.

image

Nous utilisons ensuite l'objet Dispatcher associé à la fenêtre afin de modifier la source de la CollectionViewSource dans le bon thread (celui de l'interface graphique).

Pour rappel, cette requête ne renverra pas toutes les catégories existantes en base car le service contient un « queryinterceptor » pour la collection des catégories qui ne renvoie que les catégories ayant au moins un produit associé.

III-A-5. Chargement paginé des produits

Lorsque la catégorie sélectionnée change nous voulons afficher les produits associés.

Dans la méthode associée à l'évènement SelectionChanged de la liste déroulante nous récupérons la catégorie sélectionnée. L'objet Category possède une propriété Products représentant les produits associés. Nous allons demander au contexte de charger cette propriété automatiquement (et en asynchrone). Pour cela, le contexte possède la méthode BeginLoadProperty utilisant le même pattern asynchrone que vu au-dessus.

Chargement des produits en fonction de la catégorie sélectionnée
Sélectionnez
private void cbxCategories_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    Category category = cbxCategories.SelectedItem as Category;
    if (category != null && !category.Products.Any())
    {                
        ctx.BeginLoadProperty(category, "Products",
            delegate(IAsyncResult ar)
            {
                Dispatcher.Invoke((Action)delegate { 
                    ctx.EndLoadProperty(ar);
                    //on désactive le bouton s'il n'y a pas/plus de produits à charger
                    btnNextPage.IsEnabled = category.Products.Continuation != null;
                });
            },
            null);
    }
    btnNextPage.IsEnabled = category.Products.Continuation != null;
}

La collection Products possède une propriété Continuation qui contient l'URI vers la page de résultats suivante. Si elle est « null » alors c'est que nous somme arrivé à la dernière page et nous pouvons désactiver le bouton de chargement de la page suivante sur l'interface graphique.

image


Pour le chargement des pages de résultats suivantes nous utilisons la méthode générique BeginExecute du contexte qui exécute la requête correspondant à l'URI passé en paramètre. L'URI sera ici celui de la page suivante obtenue via la propriété Continuation. Il s'agit encore d'une fois d'une méthode asynchrone.

Chargement paginé des produits
Sélectionnez
private void btnNextPage_Click(object sender, RoutedEventArgs e)
{
    Category category = cbxCategories.SelectedItem as Category;

    if (category == null || category.Products.Continuation == null)           
        return; 
                    
    // on désactive le bouton le temps du chargement
    btnNextPage.IsEnabled = false;

    // version asynchrone
    ctx.BeginExecute<Product>(category.Products.Continuation.NextLinkUri,
            delegate(IAsyncResult ar)
            {
                var products = ctx.EndExecute<Product>(ar);
                Dispatcher.Invoke((Action)delegate { 
                    category.Products.Load(products);
                    // réactiver le bouton s'il y a encore une page
                    btnNextPage.IsEnabled = category.Products.Continuation != null;
                });
            },
            null);
}

Pour un chargement automatique de tous les produits (sans intervention de l'utilisateur) vous pouvez utiliser une simple boucle whilequi exécutera autant de requêtes que nécessaire.

III-A-6. Suppression d'un produit

La suppression d'une entité se fait via la méthode DeleteObject du contexte qui marque l'entité comme étant supprimée. Lors du prochain appel à la méthode SaveChanges, le contexte enverra une requête HTTP DELETE sur l'URI de l'entité afin de la supprimer en base.

Dans notre cas, la tentative de suppression est condamnée à échouer car le service possède un « change interceptors » interdisant la suppression d'un produit.

Suppression d'un produit
Sélectionnez
private void btnDelete_Click(object sender, RoutedEventArgs e)
{
    Product p = productsListView.SelectedItem as Product;

    if (p != null)
    {
        try
        {
            ctx.DeleteObject(p);
            ctx.SaveChanges();
        }
        catch (Exception ex)
        {
            if (ex.InnerException is DataServiceClientException)
            {
                // Parse the DataServieClientException
                DataServiceErrorInfo innerException =
                    ParseDataServiceClientException(ex.InnerException.
                    Message);
                // Display the DataServiceClientException message
                if (innerException != null)
                    MessageBox.Show(innerException.Message);
            }
            else
            {
                MessageBox.Show("The delete operation throws the error: " + ex.Message);
            }
        }
    }
}


ParseDataServiceClientException est une fonction utilitaire permettant d'extraire le message d'erreur au format XML renvoyé par le service :

Erreur renvoyée par le service
Sélectionnez
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
  <code></code>
  <message xml:lang="fr-FR">Products cannot be deleted; instead set the DiscontinuedDate property</message>
</error>

Dans le cas de notre service la suppression logique d'un produit consiste à le désactiver en lui assignant une date de fin de commercialisation.

Pour mettre à jour une entité nous utilisons la méthode UpdateObject qui indique au contexte que l'entité a été modifiée. Une demande de mise à jour sera envoyée au prochain appel de la méthode SaveChanges.

Abandon d'un produit
Sélectionnez
private void btnDiscontinue_Click(object sender, RoutedEventArgs e)
{
    Product p = productsListView.SelectedItem as Product;

    if (p != null)
    {                
        try
        {
            p.DiscontinuedDate = DateTime.Now;
            ctx.UpdateObject(p);
            ctx.SaveChanges();
        }
        catch (Exception ex)
        {
            if (ex.InnerException is DataServiceClientException)
            {
                // Parse the DataServieClientException
                DataServiceErrorInfo innerException =
                    ParseDataServiceClientException(ex.InnerException.
                    Message);
                // Display the DataServiceClientException message
                if (innerException != null)
                    MessageBox.Show(innerException.Message);
            }
            else
            {
                MessageBox.Show("The delete operation throws the error: " + ex.Message);
            }
        }
    }
}

III-A-7. Création d'un nouveau produit

Pour l'ajout d'un nouveau produit nous allons devoir faire attention aux relations liant une entité « Product » à une entité « Category ».

Ajout d'un nouveau produit
Sélectionnez
private void btnNew_Click(object sender, RoutedEventArgs e)
{
    Category c = cbxCategories.SelectedItem as Category;
    if (c != null)
    {
        try
        {
            Product p = new Product();
            p.Name = "Nouveau produit " + DateTime.Now.Ticks.ToString();
            p.Color = "Black";
            p.ListPrice = 599.99M;
            p.StandardCost = 480;
            p.ProductNumber = "N-" + DateTime.Now.Ticks.ToString();
            p.SellStartDate = DateTime.Now;
            p.rowguid = Guid.NewGuid();
            p.ModifiedDate = DateTime.Now;
            p.Size = "M";

            // On définit les liens entre le produit et la catégorie
            ctx.AddRelatedObject(c, "Products", p);

            // Equivalent à la ligne précédente:
            //ctx.AddToProducts(p);
            //ctx.SetLink(p, "Category", c);                    
            //ctx.AddLink(c, "Products", p);

            // Obligatoire car non fait automatiquement
            p.Category = c;
            c.Products.Add(p);
                    
            ctx.SaveChanges();
        }
        catch (Exception ex)
        {
            // Gestion de l'erreur
        }
    }
}

La méthode AddRelatedObject permet en une seule fois d'ajouter le produit au contexte et de créer un lien qui représente la relation (dans les deux sens) entre le produit et la catégorie. Elle est équivalente à l'appel successif des trois méthodes AddToProducts (ajout du produit au context), SetLink (ajout d'un lien Produit / Catégorie, un produit n'a qu'une seul catégorie) et AddLink(ajout d'un lien Catégorie / Produit, une catégorie peut avoir plusieurs produits).

AddLink et SetLink (ainsi que AddRelatedObject) définissent les liens qui doivent être créés au niveau du service OData. côté client vous devez aussi définir vous-même les relations entre les objets via les propriétés de navigation. C'est pourquoi vous devez explicitement définir la catégorie du nouveau produit (p.Category = c) et ajouter le produit à la liste des produits associés à la catégorie (c.Products.Add(p)).

Conclusion

Basé sur Atom et AtomPub, l'Open Data Protocole se veut le standard pour exposer des données, relationnelles ou non, sous forme de service et permettre à tout type de clients (sachant manipuler HTTP, XML, JSON) de les consommer.

WCF Data Services permet la mise en place rapide de services OData utilisant la technologie .NET de Microsoft. Les outils intégrés à Visual Studio (génération de code, diagramme de visualisation du modèle) facilitent la consommation de tels services et améliorent la productivité des développeurs .NET.

Sources

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

Liens

Remerciements

J'adresse ici tous mes remerciements aux membres de l'équipe de rédaction de "Developpez.com" et plus particulièrement à jacques_jean 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 toute autre information, n'hésitez pas à me contacter via le forum.