Dot.Blog

C#, XAML, Xamarin, UWP/Android/iOS

Produire et Utiliser des données OData en Atom avec WCF et Silverlight

[new:15/09/2012]OData (Open Data Protocol) est un protocole web pour l'interrogation et la mise à jour des données basé sur les technologies web comme HTTP, AtomPub et JSON. OData peut servir à exposer et à accéder aux données de plusieurs types de sources comme les bases de données, les systèmes de fichiers et des sites Web. OData peut être exposé par une grande variété de technologies comme .Net mais aussi Java et Ruby. Côté client OData est accessible par .Net, Silverlight, WP7, JavaScript, PHP, Ruby et Objective-C pour n'e citer que quelques-uns d'entre eux. Cela rend OData très attrayant pour des solutions où les données doivent être accessibles par plusieurs clients de plateformes différentes, dont Silverlight...

Qu’est ce que OData ?

C’est avant tout un protocole de partage de données basé sur Atom et AtomPub. C’est une “norme” publique publiée par Microsoft sous License OSP (Microsoft Open Specification Promise), ce qui garantit à qui veut s’en servir qu’il ne risque aucune poursuite pour violation de brevet.

Microsoft fournit plusieurs SDK pour utiliser OData avec PHP, Java, JavaScript, WebOS, .Net et IPhone.

OData vs XML

La première question qui se pose à celui qui ne connait pas encore OData est de savoir quelle différence il peut y avoir entre ce standard et un simple service Web produisant du XML. Après tout ces derniers fonctionnent bien depuis des années, que propose de plus OData ?

La réponse est simple : cela n’a rien à voir Sourire

Un service Web en XML permet certes de fournir des données, voire des méthodes pour les manipuler, mais virtuellement un service Web basé sur XML peut offrir n’importe quel type de service.

Un service web basé sur OData, comme son nom le laisse supposer, est un service très orienté données. Donc proposant de base des méthodes (au sens de moyens) directement adaptées à la manipulation de celles-ci.

S’il fallait faire une comparaison ce n’est pas entre un service web XML et un service web OData qu’il faudrait la faire : OData ressemble en fait bien plus à une surcouche de manipulation de données comme les WCF RIA Services par exemple.

Pas d’opposition OData/XML, mais une complémentarité

En réalité, OData est une surcouche qui se base sur d’autres standards pour travailler. Notamment, les données peuvent transiter soit en XML très classique, soit dans d’autres formats tels que JSon ou Atom.

On peut donc très bien avoir un service OData utilisant XML.

JSon est aujourd’hui plus “à la mode”, question de gout et de facilité de parsing (JSon passe pour moins verbeux, c’est peut-être vrai, mais je n’ai pas vu de grandes différences avec XML dans beaucoup de cas, en revanche il est plus facilement lisible par un humain que XML).

Quant à Atom c’est au départ un format basé sur XML spécialisé pour les flux RSS.

OData et REST

OData exploite l’architecture REST plutôt que SOAP. REST a été inventé par Roy Thomas dans sa thèse “Architectural Styles and the Design of Network-based Software Architecture”. C’est une architecture, donc un ensemble de règles et contraintes. REST est l’acronyme  de “Representational State Transfer”.

C’est une architecture orientée ressource. En fait toute information de base sous REST est appelée une ressource.

REST utilise une couche de communication HTTP ce qui, comme les services Web, lui permet de “passer” à peu près partout. REST utilise les verbes GET, PUT, POST, DELETE, ... pour permettre la manipulation des ressources.

Un simple service Web XML utilisant SOAP est donc très différent du point de vue architectural d’un service de données OData. Le premier est très générique, plutôt basé sur un échange de messages entre clients et serveurs, alors que OData est fortement orienté données et permet directement de les manipuler en proposant des moyens clairement définis pour ce faire.

Exposer des données OData

Avant d’utiliser des données via OData encore faut-il trouver une source ! Comme on n’est jamais si bien servi que par soi-même nous allons rapidement mettre en œuvre un serveur OData de test.

Même si notre but est la consommation de services OData dans Silverlight, la production des données est une phase essentielle et intéressante qu’il serait dommage de sauter...

Créer un serveur de données OData

Pour les besoins de cet article je vais créer un nouveau projet application Web sous Visual Studio. Le plus simple possible. Pas d’astuce ici.

Je vais ajouter à ce projet une base de données. Pour la démo j’ai opté pour une base au format SQL CE 4. Il suffit de poser le fichier dans App_Data et l’affaire est (presque) jouée. Le Server Explorer de VS propose alors la connexion à la base de donnée dans “Data Connections” (j’utilise toujours VS en anglais, je pense que vous retrouverez facilement les équivalents dans la version traduite).

La base de test est typique d’une application LOB : des clients, des commandes, etc. Peu importe, je ne vais utiliser qu’une table ici, celle des clients.

J’ai ajouté ensuite un ADO.NET Entity Model et j’ai choisi de le créer à partir d’une base existante. Le dialogue qui suit propose directement la connexion à notre base de test.

image

En quelques clics j’ai pu créer un modèle complet de la base (inutile dans cet exemple je l’avoue, au final je vais tout retirer sauf la table Customer ! ) :

image

Je n’utiliserai que la table Customer pour l’exemple.

Pour que l’application Web fonctionne si elle est déployée sur un serveur il ne faut pas oublier d’ajouter les binaires de SQL CE (ce qui n’est pas nécessaire avec une base SQL Server “normale” puisqu’elle est censée être installée sur le serveur). L’astuce consiste ici à faire un clic-droit sur le nom de l’application Web et de choisir l’entrée “Add deployable dependencies” (ajouter les dépendances déployables). Un dialogue apparait permettant de choisir d’intégrer SQL server Compact. Dès qu’on valide le répertoire “References” de l’application se charge de tous les binaires nécessaire au fonctionnement autonome de SQL CE (qui doit être au préalable installé sur votre machine de développement cela va sans dire).

Ne reste plus qu’à exposer ces données en OData ! (J’aime les jeux de mots à la noix, désolé).

- Mais comment ? On a lu tout ça pour le savoir !

Pas d’affolement c’est très simple puisque rien ne change ou presque avec un service RIA Services...

J’ai donc ajouté un nouvel item à mon application ASP.NET : un WCF Data Service.

Par défaut ce service est vide est ne sait pas quel type de données gérer :

image

On voit le souligné rouge invitant à indiquer le type de la source. Ici ce seront les entités de notre modèle Entity Framework.

Par défaut le service n’expose aucune données, c’est au développeur de préciser les règles d’accès.

Si l’application est lancée et qu’on accède au service ce dernier retournera le code suivant :

<?xml version="1.0" encoding="UTF-8" standalone="true"?>
-<service xmlns="http://www.w3.org/2007/app" xmlns:app="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom" 
  xml:base="http://localhost:2116/TestDBService.svc/"> 
  -<workspace> <atom:title>Default</atom:title> </workspace> </service>

Dans la méthode InitializeService qu’on voit dans la capture ci-dessus je vais ajouter ces règles mais avant tout il y a une astuce à connaître : comme on le voit sur la capture le paramètre “config” passé à InitializeService est de type IDataServiceConfiguration, or cette interface n’expose pas l’une des propriétés dont nous avons besoin pour fixer le protocole... Petit bug ? Je ne saurai dire. En tout cas la feinte consiste à modifier le type du paramètre et à utiliser directement DataServiceConfiguration au lieu de l’interface.

Maintenant nous pouvons exposer la table Customers et indiquer le protocole à utiliser :

namespace OdataDemo
{
public class TestDBService : DataService<TestDBEntities>
{
// This method is called only once to initialize service-wide policies.
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("Customers", EntitySetRights.AllRead);
config.UseVerboseErrors = true;
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
}
}

 

Si nous relançons le service, nous voyons apparaitre la ressource “Customers”. Et si nous accédons à cette dernière (en ajoutant “/Customers” au nom du service) Internet Explorer croyant voir un flux RSS sous Atom affichera une série de billets vides (la table Customers n’a pas de propriétés identiques à celle d’un flux RSS standard...). En revanche on voir 91 entrées qui correspondent aux 91clients de la base de test...

Rassurez-vous, avec Chrome c’est encore pire, il demande d’installer une extension pour lire les flux RSS.

En demandant de voir le source de la page nous pouvons voir les données présentées sous forme d’un flux Atom :

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<feed xml:base="http://localhost:2116/TestDBService.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">Customers</title>
  <id>http://localhost:2116/TestDBService.svc/Customers</id>
  <updated>2012-08-04T20:43:32Z</updated>
  <link rel="self" title="Customers" href="Customers" />
  <entry>
    <id>http://localhost:2116/TestDBService.svc/Customers('ALFKI')</id>
    <title type="text"></title>
    <updated>2012-08-04T20:43:32Z</updated>
    <author>
      <name />
    </author>
    <link rel="edit" title="Customer" href="Customers('ALFKI')" />
    <category term="TestDBModel.Customer" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
    <content type="application/xml">
      <m:properties>
        <d:Customer_ID>ALFKI</d:Customer_ID>
        <d:Company_Name>Alfreds Futterkiste</d:Company_Name>
        <d:Contact_Name>Maria Anders</d:Contact_Name>
        <d:Contact_Title>Sales Representative</d:Contact_Title>
        <d:Address>Obere Str. 57</d:Address>
        <d:City>Berlin</d:City>
        <d:Region m:null="true" />
        <d:Postal_Code>12209</d:Postal_Code>
        <d:Country>Germany</d:Country>
        <d:Phone>030-0074321</d:Phone>
        <d:Fax>030-0076545</d:Fax>
      </m:properties>
    </content>
  </entry>
  <entry>
    <id>http://localhost:2116/TestDBService.svc/Customers('ANATR')</id>
    <title type="text"></title>
    <updated>2012-08-04T20:43:32Z</updated>
    <author>
      <name />
    </author>
    <link rel="edit" title="Customer" href="Customers('ANATR')" />
    <category term="TestDBModel.Customer" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
    <content type="application/xml">
      <m:properties>
        <d:Customer_ID>ANATR</d:Customer_ID>
        <d:Company_Name>Ana Trujillo Emparedados y helados</d:Company_Name>
        <d:Contact_Name>Ana Trujillo</d:Contact_Name>
        <d:Contact_Title>Owner</d:Contact_Title>
        <d:Address>Avda. de la Constitución 2222</d:Address>
        <d:City>México D.F.</d:City>
        <d:Region m:null="true" />
        <d:Postal_Code>05021</d:Postal_Code>
        <d:Country>Mexico</d:Country>
        <d:Phone>(5) 555-4729</d:Phone>
        <d:Fax>(5) 555-3745</d:Fax>
      </m:properties>
    </content>
  </entry>
  <entry>
...

Bref, ici nous savons que le service fonctionne et qu’il répond.

Contrôler les données transmises

Donner l’accès, même en lecture seule (ce que la gestion des droits peut faire dans l’initialisation du service), n’est pas toujours suffisant. Il peut être nécessaire de filtrer ou contrôler les données avant de les servir au client. De nombreuses situations réclament un tel filtrage. La première est de limiter la taille du set retourné. La seconde peut être de cacher certaines données à certains utilisateurs (gestion de rôle débouchant sur des accès partiels aux informations par exemple), etc.

Dans notre exemple nous publions la table “Customers” et nous souhaitons limiter l’accès aux clients (ceux de la base de données) se trouvant en France.

Il suffit de détourner les requêtes automatiques du service OData pour y insérer le filtre. Cela s’effectue de la façon suivante :

[QueryInterceptor("Customers")]
        public Expression<Func<Customer,bool>> OnQueryCustomers()
        {
            return c => string.Compare(c.Country, "France", 
                             StringComparison.InvariantCultureIgnoreCase) == 0;
        }

L’attribut permet d’indiquer quelle ressource on souhaite filtrer, le code de la méthode créé le filtrage (ici sur le nom du pays).

C’est vraiment très simple non ?

Encore faut-il maintenant consommer ces données côté Silverlight !

Le client Silverlight

Après avoir ajouter une application Silverlight à la solution en cours, il faut ajouter la référence au service :

image

Le reste est automatique puisque tout ce qu’il faut pour accéder au service est généré dans l’application Silverlight.

Visuellement l’application se limite à une simple DataGrid et un bouton “Load”. Quand le bouton est cliqué les données sont chargées depuis le service distant et affichées dans la grille.

Le code Xaml de la MainPage est le suivant :

<UserControl x:Class="Client.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="800" d:DesignWidth="1024"
     xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">

    <Grid x:Name="LayoutRoot" Background="White">
        <sdk:DataGrid AutoGenerateColumns="True" Height="585" 
                HorizontalAlignment="Left" Margin="12,12,0,0" Name="dataGrid1" 
                VerticalAlignment="Top" Width="1000" />
        <Button Content="Load" Height="23" HorizontalAlignment="Left" 
            Margin="937,614,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
    </Grid>
</UserControl>

Je ne vous mentais pas, c’est vraiment minimaliste !

Quant au code C# le voici :

public partial class MainPage : UserControl
   {
       public MainPage()
       {
           InitializeComponent();
       }

       private DataServiceCollection<TestDBService.Customer> customers;

       
       private void LoadCustomers()
       {
           var url = "http://localhost:2116/TestDBService.svc/";
           var uri = new Uri(url, UriKind.Absolute);
           var service = new TestDBService.TestDBEntities(uri);
           customers = new DataServiceCollection<Customer>(service);
           var query = service.Customers;
           customers.LoadCompleted += 
                  new EventHandler<LoadCompletedEventArgs>(customers_LoadCompleted);
           customers.LoadAsync(query);
       }

       void customers_LoadCompleted(object sender, LoadCompletedEventArgs e)
       {
           if (e.Error!=null)
           {
               MessageBox.Show(e.Error.Message);
               return;
           }
           dataGrid1.ItemsSource = customers;
       }

       private void button1_Click(object sender, RoutedEventArgs e)
       {
           LoadCustomers();
       }
   }

On voit qu’ici aussi il n’y a pas de quoi s’affoler...

Une variable de type DataServiceCollection typée selon la classe “Customer” est créée. Elle contiendra les données retournées.

Le clic sur le bouton déclenche la méthode LoadCustomers() dont le code est facile à suivre :

L’Uri est créée et une instance du service l’est aussi en lui passant l’Uri en paramètre. Ensuite c’est une instance de la DataServiceCollection qui est créée. Une requête est créée comme étant l’ensemble des données de la ressource Customers (filtrée par le serveur, mais ça le client ne peut ni le savoir ni le changer). Nous aurions pu créer une requête LINQ plus complexe, mais ce n’est pas le sujet de cet article. Donc autant rester simple.

Puis le “LoadCompleted” de la DataServiceCollection est pointé vers la méthode qui accusera réception des données quand elles arriveront (c’est de l’asynchrone, ne l’oublions pas... Avec les nouveaux mots clé await et async de la version à venir de C# cette programmation serait beaucoup plus limpide).

Enfin, la demande est faite à la collection de charger le résultat de la requête. A partir de là le service est interrogé, le serveur va répondre, traiter la requête, le code de filtrage que nous avons ajouté limitera la réponse aux clients se trouvant en France, et la collection de données sera renvoyée à Silverlight.

C’est là que Silverlight et tout son code simplifiant le travail attrapera les données dans ses petits bras musclés, moulinera tout cela, et déclenchera la méthode customers_LoadCompleted. Nous n’ajoutons que le strict minimum vital : tester si il y a ou non un erreur. Dans l’affirmative nous affichons le message d’erreur, dans la négative l’ItemsSource de la grille de données est initialisée pour pointer la DataServiceCollection.

Ce n’est qu’un code de démonstration et non un exemple pour une application de production.

En relançant l’application Web (dont nous avons positionné la page de démarrage à la page test de l’application Silverlight et non plus sur le service) nous obtenons, après un clic sur le bouton “Load”, l’affichage suivant :

image

 

C’est du brut de fonderie, mais ça marche. Nous avons requêté et reçu des données distantes produites par un service OData...

Bien entendu, selon le même scénario et puisque nous avons mis la table Customers en mode d’accès read/write (au niveau de l’initialisation du service dans le code côté serveur) nous pourrions extrapoler jusqu’à gérer la modification des données, la DataGrid pouvant être directement utilisée dans ce sens.

Ce n’est qu’un billet et non un livre, il faut bien que je mette une limite...

L’aspect sécurité

D’autant qu’il faut dire un mot sur la sécurité.

Il semble en effet bien imprudent d’ouvrir comme cela les vannes de ses précieuses données sans visiblement qu’aucun mécanisme d’identification du client ne soit en place.

Parfois cela est voulu : publication de données, de statistiques en lecture seule, d’un catalogue de produits, d’un flux d’informations etc.

Souvent en entreprise ce n’est pas du tout souhaitable. L’accès aux données doit être contrôlé.

Dans un tel cas il est nécessaire de pouvoir identifier chaque client avant d’accepter sa requête. Il existe plusieurs solutions, la sécurisation des services WCF est un sujet délicat et pointu.

C’est pourquoi plutôt que de bâcler un exemple peu réaliste je souhaitais surtout ici appuyer sur la nécessité de sécuriser ses services de données. A vous de vous plonger dans les méandres de WCF et de sa sécurité, il existe des tonnes de choses sur ce sujet qui sortirait totalement du cadre de ce billet.

Il y a bien sur des méthodes simples, comme passer des informations d’identification par le header de la requête HTTP entre le client et le serveur. Cela peut suffire pour des données peu vitales ou un service publié en intranet (le client envoie le nom Windows de l’utilisateur et le serveur vérifie dans une liste que cet utilisateur est autorisé par exemple). Pour une véritable sécurisation de niveau professionnelle d’un service en lecture/écriture publié sur le Web, c’est une autre affaire. Consulter un expert en sécurité est alors le meilleur conseil qu’un expert Silverlight peut vous donner !

Conclusion

Nous avons créé ici un serveur de données OData ainsi qu’un client Silverlight sachant les consommer et les présenter à l’utilisateur.

C’est peu... mais c’est énorme !

Le but de cet article n’est pas d’être un cours, ni sur OData, ni rien d’autre. Il a juste pour objectif de vous montrer à quel point la création de données OData est simple tout autant que de consommer de telles données.

Si je vous ai donné l’eau à la bouche, si j’ai réussi à faire en sorte que OData n’est plus mystérieux pour vous, alors je serai déjà pleinement satisfait, je ne visais rien d’autre !

La solution complète Visual Studio 2010 (avec la base de test SQL CE 4) est téléchargeable ici :

Stay Tuned !

blog comments powered by Disqus