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 :
<?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 » :
<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 :
<?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 » :
<?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 modèle 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 sont 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 :
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.) :
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 :
I-C-2-a. 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-b. 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 :
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é :
<?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-c. 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. |
$top |
Permet de restreindre le nombre d'entrées retournées. Utilisé avec l'option $skip, il permet d'implémenter de la pagination. |
$skip |
Permet de sauter un nombre donné d'entrées du résultat. L'option $skip s'utilise sur des listes 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. |
$filter |
Permet d'appliquer une expression de filtre sur une collection d'entrée. |
$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 par exemple requêter une liste de produits et inclure les catégories associées au sein d'une même requête HTTP. |
$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. |
$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. |
$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. |
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 :
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.
En analysant le résultat, on voit que celui-ci a bien été renvoyé 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.
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 ».
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 :
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.
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 :
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.
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 :
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 :
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'autohébergement.
Dans notre exemple nous utiliserons le template WCF Service Application et ciblerons le Framework 4.0.
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 modèle 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.
Créez une connexion vers la base AdventureWorks :
Puis sélectionnez les tables Product, ProductCategory et ProductModel :
Vous devriez obtenir un modèle proche de celui-ci (après avoir renommé quelques propriétés) :
II-B. Création d'un projet WCF Data Services▲
Nous allons maintenant exposer le modèle 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.
La solution doit maintenant ressembler à ceci :
Le fichier AdventureWorksDataService.svc contient les instructions pour le runtime WCF afin qu'il instancie le service OData via un objet de type DataServiceHostFactory.
<%@ 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 :
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-a. 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 :
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-b. 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 :
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:
Seules 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êtes 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 modifiant 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 :
// 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 :
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 collection des produits :
// 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 :
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
[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:
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 :
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 de 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 :
// 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 affecteront la mise en cache. Le caractère « * » indique que l'ensemble des paramètres affecteront 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 suivant :
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, exceptions 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.
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 :
<?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 :
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) :
<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. Présentation de l'application▲
Nous allons créer une application WPF avec l'interface graphique suivante :
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-B. 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.
Le proxy s'instancie en passant en paramètre un URI vers le service WCF Data Services :
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-C. 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).
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 :
Voici le diagramme généré dans le cas du service nous servant d'exemple :
III-D. 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.
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.
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-E. 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.
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 sommes arrivés à la dernière page et nous pouvons désactiver le bouton de chargement de la page suivante sur l'interface graphique.
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.
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-F. 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.
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 :
<?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.
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-G. 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 ».
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 seule 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.