ADO.NET Data Service: Une architecture REST en dix minutes

22 Fév

Dans les architectures modernes, la tendance est de fabriquer des services qui vont être exploités par différents outils (IHM ou autres). Ainsi, une couche expose des services qui vont être exploités pour effectuer des opérations dans le système d’information.

Il existe plusieurs possibilités pour répondre à ce besoin. L’architecture de type REST en est une.

REST signifie « Representational State Transfer ». Concrètement, REST c’est:

  • des URI pour référencer les ressources,
  • HTTP comme protocole de transfert,
  • les méthodes standards HTTP pour effectuer des opérations(POST, GET, bien connues, mais aussi DELETE, PUT),
  • des messages au format SOAP/JSON.

Je vais montrer dans cet article comment créer facilement une architecture REST à partir de Visual Studio, en utilisant les composant ADO.NET, que je vais utiliser avec un client lourd (WINFORMS) et un client léger(ASP.NET MVC2).

Le matériel

Pour faire cela, il faut:

  • Visual Studio 2008, avec SP1,
  • le framework .net 3.5 avec SP1,
  • MS SQL Serveur 2008 Express (il faut SP3 sur une machine XP pour l’installer, enfin, pour installer PowerShell…).

L’exemple sera codé en C#.

Architecture

Voici globalement l’architecture utilisée dans ce billet, chaque couche étant matérialisée par un projet:

Une architecture basée sur ADO.NET Data Service

ADO.NET Entity Framework permet de faire une sorte d’ORM, made in Microsoft, simplifié. Il permet de mapper une base de données relationnelles avec des objets (POCO: C# ou VB) qu’il est possible d’utiliser dans un projet et de donner à ceux-ci une couche de persistance.

ADO.NET Data Service permet de mettre à disposition la couche Entity par le biais de service REST. L’Uri mis à disposition par la couche service est utilisée du coté client pour accéder aux différents services.

Dans les couches clientes, le service sera simplement déclaré, ce qui permettra d’utiliser les services(création automatique du proxy et des beans utilisables).

Création de la base de données

Ce billet a pour but de montrer comment utiliser VS pour faire une architecture REST. Le schéma utilisé est donc très simple: un table « produit » qui fait une référence à une table des utilisateurs:

Le shéma utilisé

Création de la couche DAO: ADO.NET Entity

La première étape consiste à créer la solution et la première couche. Pour cela faire « File » > « New » > « Project » et choisir « Bibliothèque de classe »:

Créer la solution

Créer le DAO: Faire un clic droit sur le projet, puis « Add », « Nouvel élément » et choisir « ADO.NET Entity Data Model ». Nommer « ProductEntity.edmx » pour le fichier qui contient la définition du DAO.

Faire la connection: Dans le wizard suivant, faire la nouvelle connexion à la base qui permet de se connecter au schéma précédemment créé. Cocher « Save entity connection setting in App.config as: » et référencer « ProductEntities ».

Choisir les tables à mapper: et donner un espace de nommage:

Dans le dernier wizard, choisir les tables à mapper

Et voila. Le fichier .edmx créé contient toutes les informations concernant le mapping, les objets créés:

Tout le bouzin dans le fichier .edmx

C’est tout, ça marche.

Création de la couche service

Faire un clic droit sur la solution et « Add » > « Nouveau projet ». Choisir « ASP.NET Web Service Application », pour créer des webservices:

Création de la couche service

Ajouter un référence au projet de dao(clic droit sur le projet, « Ajouter une référence » et choisir le projet de dao dans l’onglet « Projet »).

Il faut ensuite ajouter la gestion des services mis à disposition par ADO.NET Data Service: faire un clic droit sur le projet, « Add », « Nouvel élément » et choisir « ADO.NET Data Service »:

Déclaration de la partie ADO.NET Data Service

Le fichier ProductService.svc sera appelé de l’extérieur pour utiliser les services contenus dans le projet. Il faut l’éditer car il permet de configurer:

  • les entités à publier par ce service,
  • les droits sur les entités et les méthodes à publier.

Je vais donner tous les droits, pour l’exemple:

using System;
 using System.Collections.Generic;
 using System.Data.Services;
 using System.Linq;
 using System.ServiceModel.Web;
 using System.Web;
 using fr.adioss.rest.dao;
 
namespace fr.adioss.rest.service
 {
 // déclarer entre <> le dao
 public class ProductService : DataService<ProductEntities>
 {
 // déclaration des méthodes et objets exposés
 public static void InitializeService(IDataServiceConfiguration config)
 {
 //donner l'accès avec tous les droits sur tous les objets dao
 config.SetEntitySetAccessRule("*", EntitySetRights.All);
 //donner accès avec tous les droits sur tous les services
 config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
 }
 }
 }

Finitions: Faire un clic droit sur le projet, propriété et Web puis configurer le port (pour moi: 8080 dans « Specific port »).

Compiler et lancer. A l’url http://localhost:8080/ProductService.svc/, dans FireFox par exemple, on peut voir que le service tourne correctement:

Les classes distantes User et Product sont publiées

Remarque:

Si comme moi vous avez fait deux projets (il aurait été possible d’en faire un seul de type « ASP.NET Web Service Application » et mettre le dao et le service dedans: le problème ne se pose pas alors), vous aurez certainement l’erreur suivante:

« La connexion nommée spécifiée est introuvable dans la configuration, n’est pas destinée à être utilisée avec le fournisseur EntityClient ou n’est pas valide. »

Pour la corriger, ouvrir le fichier App.Config du projet dao, copier la connexion string:

<connectionStrings>
 <add name="ProductEntities" connectionString="metadata=res://*/ProductEntity.csdl|res://*/ProductEntity.ssdl|res://*/ProductEntity.msl;provider=System.Data.SqlClient;provider connection string=&quot;Data Source=test_sgbdSQLEXPRESS;Initial Catalog=Task;Integrated Security=True;MultipleActiveResultSets=True&quot;" providerName="System.Data.EntityClient" />
</connectionStrings>

puis, dans le fichier web.config du projet service, rechercher « connectionStrings » et modifier par les données précédentes.

Création de la couche Front: WINFORMS

Sur la solution faire « Add » et ajouter un nouveau projet « Windows Forms »:

Création du projet Front avec WINFORMS

Pour utiliser les services de la couche service, faire un clic droit sur le projet et faire « Ajouter un référence de service ». Il suffit de mettre « http://localhost:8080/ProductService.svc » pour découvrir le service:

Ajout de la référence vers les services contenues dans la couche service

Le proxy et les objets associés sont maintenant accessibles. C’est pas plus compliqué.

Rajouter, dans ma forms, une gridview et trois bouttons: ajouter, éditer et supprimer. Une autre form permet d’éditer et d’ajouter des produits:

L'IHM avec WinForms

Dans le code:

La form principale (Form1.cs):

namespace fr.adioss.rest.form.winforms
{
 public partial class Form1 : Form
 {
 private ProductEntities context;
 
 private void dataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e) {}
 private void Form1_Load(object sender, EventArgs e) {}
 
 // datasource de la grid
 public void refreshDataGridView()
 {
 // récupération par le proxy de la liste des produits:
 
 // méthode 1: ToList direct
 //      dataGridView1.DataSource = context.Product.ToList<Product>();
 // le problème est que l'on ne récupére que l'objet et pas les objects liés(User): pas de left outer join
 
 // méthode 2: append simple avec requete LINQ et boucle sur les données
 var query = from tmp in context.Product.Expand("User") select tmp;
 DataTable result = new DataTable();
 DataColumn productIDCol = new DataColumn();
 productIDCol.DataType = System.Type.GetType("System.String");
 productIDCol.ColumnName = "ProductID";
 result.Columns.Add(productIDCol);
 DataColumn categorieCol = new DataColumn();
 categorieCol.DataType = System.Type.GetType("System.String");
 categorieCol.ColumnName = "Nom de la description";
 result.Columns.Add(categorieCol);
 DataColumn userCol = new DataColumn();
 userCol.DataType = System.Type.GetType("System.String");
 userCol.ColumnName = "User";
 result.Columns.Add(userCol);
 DataRow dataRow = null;
 foreach (Product p in query){
 dataRow = result.NewRow();
 dataRow["ProductID"] =  Convert.ToString(p.ProductID);
 dataRow["Nom de la description"] = p.Name;
 try
 {
 dataRow["User"] = p.User.Prenom + " " + p.User.Nom;
 }
 catch (Exception e)
 {
 }
 result.Rows.Add(dataRow);
 }
 dataGridView1.DataSource = result;
 }
 
 public Form1()
 {
 InitializeComponent();
 this.MdiParent = null;
 // !!! initialisation du proxy vers le service !!!
 context = new ProductEntities(new Uri("http://localhost:8080/ProductService.svc"));
 context.MergeOption = MergeOption.AppendOnly;
 refreshDataGridView();
 }
 
 private void AddButton_Click(object sender, EventArgs e)
 {
 Edition edition = new Edition();
 edition.MdiParent = this;
 edition.context = context;
 edition.Show();
 }
 
 private void EditButton_Click(object sender, EventArgs e)
 {
 String id = dataGridView1["ProductID", dataGridView1.SelectedCells[0].RowIndex].Value.ToString();
 Edition edition = new Edition();
 edition.MdiParent = this;
 edition.id = id;
 edition.context = context;
 edition.Show();
 }
 
 private void DeleteButton_Click(object sender, EventArgs e)
 {
 String id = dataGridView1["ProductID", dataGridView1.SelectedCells[0].RowIndex].Value.ToString();
 Product tmp = (context.Product.Where(p => p.ProductID == Convert.ToInt32(id))).First<Product>();
 context.DeleteObject(tmp);
 context.SaveChanges();
 refreshDataGridView();
 }
 }
}

La forme pour l’édition(Form2.cs):

namespace fr.adioss.rest.form.winforms
 {
 public partial class Edition : Form
 {
 public String id;
 public ProductEntities context;
 
 public Edition()
 {
 InitializeComponent();
 }
 
 private void Edition_Load(object sender, EventArgs e)
 {
 //alimenter la combobox avec la lite des users
 UserComboBox.DataSource = context.User.ToList<User>();
 UserComboBox.DisplayMember = "Nom";
 UserComboBox.ValueMember = "ID";
 //si l'id n'est pas nul, on recupere le produit et on sette les données
 if (id != null && id != "")
 {
 // récupération par requète du produit
 Product tmp = (context.Product.Expand("User").Where(p => p.ProductID == Convert.ToInt32(id))).First<Product>();
 textBox1.Text = tmp.Name;
 textBox2.Text = tmp.Category;
 try
 {
 UserComboBox.SelectedValue = tmp.User.ID;
 }catch(Exception ex){}
 }
 }
 
 private void SaveButton_Click(object sender, EventArgs e)
 {
 //instanciation de l'objet(créer avec la référence au service)
 Product tmp = null;
 User tmpUser = null;
 
 if (id != null && id != "")
 {
 tmp = (context.Product.Where(p => p.ProductID == Convert.ToInt32(id))).First<Product>();
 }else{
 tmp = new Product();
 }
 tmp.Name = textBox1.Text;
 tmp.Category = textBox2.Text;
 tmpUser = (context.User.Where(p => p.ID == Convert.ToInt32(UserComboBox.SelectedValue))).First<User>();
 tmp.User = tmpUser;
 //ajout au proxy
 if (id != null && id != "")
 {
 tmp.ProductID = Convert.ToInt32(id);
 context.UpdateObject(tmp);
 }
 else
 {
 context.AddToProduct(tmp);
 }
//liaison user/product
context.SetLink(tmp, "User", tmpUser);
 try
 {
 //enregistrement des données en base: lancement de la commande
 context.SaveChanges();
 }
 catch (Exception ex)
 {
 MessageBox.Show(ex.Message);
 }
 finally
 {
 ((Form1)this.MdiParent).refreshDataGridView();
 this.Close();
 }
 }
 
 private void UndoButton_Click(object sender, EventArgs e)
 {
 this.Close();
 }
 }
}

Et voilà.

Utilisation de méthodes distantes

Pour afficher la liste dans la datagrid,  la méthode 2 dans le code de « form1.cs » fonctionne mais elle comporte un certain nombre d’incovénients:

  • trop de lignes de code(fainéant!) et performance diminuée,
  • logique embarquée dans la vue(et si l’on veut réutiliser ce datasource, par exemple dans un autre frontale, il faut ré-écrire le code…).

Il est possible de faire beaucoup plus simple:

  • en codant une requête dans une classe et la déclarer dans ProductService.svc(publication en service) dans la couche service,
  • en faisant une vue et en l’ajoutant au fichier edmx: la solution la plus simple et rapide!

Pour cela, il faut créer une vue:

CREATE VIEW [dbo].[ListProductUser]
AS
SELECT     dbo.Product.ProductID, dbo.Product.Name, dbo.Product.Category, dbo.[USER].Nom + ' ' + dbo.[USER].Prenom AS Nom
FROM         dbo.Product LEFT OUTER JOIN
 dbo.[USER] ON dbo.Product.UserID = dbo.[USER].ID
GO

L’enregistrer et la nommer ListProductUser par exemple.

Il faut l’ajouter au fichier edmx du projet dao. Pour cela, ouvrir ce fichier, faire un clic droit et « Update model from database ». Dans l’onglet « Add », choisir la vue précédemment créée. La vue est disponible et le dao/entités sont mis à jour.

Maintenant, dans le projet front, dans « Service Reference », faire un clic droit sur « ProductServiceReference », la déclaration de notre référence aux services web. Faire « Mettre à jour la référence de service ». La vue est maintenant disponible dans notre référence.

Pour alimenter la datagrid, c’est beaucoup moins long puisque, la méthode étant utilisable par le proxy, il suffit de faire:

dataGridView1.DataSource = context.ListProductUser.ToList<ListProductUser>();

Création de la couche Front: ASP.NET MVC2

Sur la solution faire « Add » puis « Nouveau projet ». Choisir « ASP.NET MVC 2 Web Application »:

La couche Front MVC

Faire un clic droit sur le projet et « ajouter une référence de service ». Faire pointer sur l’url http://localhost:8080/ProductService.svc/

Dans le dossier View/Home, supprimer la page Index.aspx. Dans Controlers, supprimer le HomeController.cs. Sur ce dossier, faire ensuite « Add » puis « Controller »:

Création du controller

dans le code, rajouter/remplacer:

private ProductEntities context = new ProductEntities(new Uri("http://localhost:8080/ProductService.svc"));
 
 public ActionResult Index()
 {
 //la vue va être utilisée pour afficher la liste
 return View(context.ListProductUser.ToList());
 }

Faire un clic droit sur cette méthode et faire « Add View », et faire la vue pour la liste:

Création de la page aspx en injectant la vue, par le proxy

Ici, c’est « ProductEntities » qui est déclaré pour que la vue générée soit pratiquement opérationnelle.

En revanche, le contrôleur injecte dans la request un objet de type ListProductUser. Il faut donc faire la modification suivante dans la page aspx:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<fr.adioss.rest.front.mvc.ProductServiceReference.ListProductUser>>" %>

Compile and play:

Le résultat. Le nom/prénom sont dans la request et peuvent être ajoutés

Il en est de même pour la création des autres vues et pour le reste du paramétrage du contrôleur.

Conclusion

Pour faire une application basée sur une architecture modulaire, l’architecture REST est une bonne approche. Microsoft répond bien à la demande en fournissant avec VS des fonctionnalités permettant de mettre en place celle-ci en très peu de temps.

Les avantages du REST par DOT.NET Entity framework et Data Service:

  • rapidité de mise en place,
  • marche « one shot »,
  • facile à comprendre,
  • intégration dans VS.

Inconvénients:

  • problèmes avec les querys LINK à partir du proxy, qui ne marchent pas avec certains paramètres (et du coup, les requêtes adéquates ne sont pas forcément réalisables),
  • dans ADO.NET Entity, je trouve que pour faire une véritable application en couche, le DAO et le domaine sont trop fortement couplés(fichier edmx), et ça, c’est mal,
  • pas méga, méga bien documenté quand on veut rentrer dans le détail, en fait.

Je n’ai pas encore tester la sécurité(toutes entités et fonctions sont accessibles de part le monde: pas top), notamment qui a le droit le lire/enregistrer supprimer, donc je ne peux pas trop me prononcer à me sujet, mais je ferai un autre billet à ce sujet.

liens:

ADO.NET Data Services Viewer Tool

ADO.NET Data Services Framework chez Microsoft

La forme pour l'édition

5 thoughts on “ADO.NET Data Service: Une architecture REST en dix minutes

  1. Re salut 😉
    Peux tu expliquer un peu plus ce qu’est l’architecture REST ?
    Pour moi c’est surtout une architecture « sans état ». Dans le même temps si tu peux faire une comparaison à l’implémentation REST Java EE6 : Glassfish / Jersey, ca pourrait donner un bon apercu de la différence DotNet/Java.
    Oui oui je sais, j’en demande beaucoup 😉

    • Merci pour ton retour. Effectivement, ça pourrait être pas mal et les journées sont bien trop courtes… Attention: ce billet n’est pas destiné à expliquer une architecture REST, et la comparer avec d’autres, mais de montrer l’intégration dans Visual Studio. Si j’ai tapé à coté et qu’un billet sur REST et la comparaison avec d’autres archis serait plus intéressant, c’est un coup dans l’eau, mais c’est partie remise 🙂
      ps: On va à la JUG demain? Cela parle EE6, je crois que ça t’interesse un peu 🙂
      Soirée JavaEE6

  2. C’est moi qui me suis mal exprimé 😉 Je parlais d’une comparaison sur l’intégration REST/DOTNET REST/JAVAEE6 😉
    Après REST ya rien à comparer. Tu fais des demandes sans maintient d’état en fournissant des formats différents (byte / xml / json etc).

    Sinon pour le JUG non, je suis pas dispo. Dommage, vu que j’y étais allé la dernière fois et que Antonio Goncalves était malade .. 😉

    • Alors, on est d’accord! Je vais te dire un truc, c’est dans le pipe! J’ai deux billets en priorité 1 et ensuite je m’attaque à ça.
      C’est quoi qui serait le plus intéressant? Avoir une comparaison des possibilités des deux? L’intégration avec les IDE? Rapidité de dev? WCF vs Glassfish, le combat? Si t’as des idées(je sais que t’en as!), n’hésite pas!

  3. Salut les amis,

    il est clair que c’est plutôt élégant comme architecture. L’opposition selon moi se situe plus au niveau des « styles » d’architectures d’exposition de services :

    – REST qui expose directement les ressources en XML ou mieux en JSON (pour manipulation directe depuis un composant Javascript de type AJAX) >> les pionniers à exposer leurs ressources de cette façon simplifiée ont été AMAZON et E-BAY il y a 4 ou 5 ans.

    – SOAP qui est un protocole à lui tout seul et qui se voulait une initiative ambitieuse de normalisation via les WS-* de tous les aspects d’une architecture de services (sécurité, gouvernance…).

    Du coup dans la définition que tu donnes à REST, je ne me retrouve pas dans le dernier item quand tu parles de SOAP (mais c’est un détail).

    Concernant les implémentations JEE6 de Glassfish, JBoss 6 et Resin 4 (voire un jour Tomcat), il est clair qu’il y a de la matière pour de beaux futurs posts.

    J’étais donc au JUG Grenoble de février : un gros succès avec 70 inscrits en pleines vacances scolaires. La prez d’Antonio était vraiment claire et complète. En plus j’ai gagné une licence du wiki d’Atlassian !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *