|
Fonctionnellement parlant, il s'agira de réaliser une suppression transactionnelle d'employés en base de données, de sérialiser dans la même transaction ceux supprimés dans un fichier xml côté client et de gérer les ajouts de nouveaux employés. Le client du service sera l'instigateur de la transaction.
Voici le schéma de la table en base de données. Pour les plus attentifs, vous reconnaitrez la base de données Northwind et plus particulièrement la table Employee :
 Table Employees
Voici la cartographie de la solution. Nous retrouvons un découpage en couche assez classique :
 Solution Visual Studio 2008
Visual Studio 2008 apporte de nombreuses nouveautés au niveau WCF et notamment l'auto-host des projets de type WCF Library. Cela permet de lancer automatiquement un processus appelé « WcfSvcHost.exe » (C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE) chargé de démarrer les écoutes actives sur tous les services. Je n'ai pas utilisé « WcfSvcHost », car je souhaitais associer une Base de données SQL Express au processus porteur. De plus, cette solution bien que très pratique pour tester et débugger rapidement devient moins intéressante en phase de déploiement où il faudra quand même finir par développer le projet de processus porteur et y intégrer le fichier de configuration qui aura été généré dans le projet des services WCF (une bibliothèque de classe) qu'il faudra fusionner avec l'app.config (ou le web.config) du host.
L'IHM de l'application est volontairement restée simple. J'ai complètement laissé de côté l'ergonomie et la partie « Mickey » (graphisme) car cela n'était pas l'objet de l'article :
 IHM de l'application
A partir de là, plusieurs étapes ont été nécessaires : - La mise en place du contexte transactionnel
La transaction démarrera du côté du client par :- La sérialisation en XML dans un fichier des employés à supprimer dans le répertoire d'exécution
- La suppression en base des employés sélectionnés pour suppression (case cochée)
- L'ajout en base des nouveaux employés (y compris l'image)
Il est important de noter que je n'ai pas géré les mises à jour des employés, même si elles sont possibles via l'interface (sans sauvegarde).
Le contexte transactionnel faisant intervenir des « Resources Managers » de types différents, il est apparu logique d'utiliser un TransactionScope pour le mettre en place. Voici donc le code afférent :
 Code de mise en place de la transaction
On peut se demander à juste titre pourquoi il y a obligation d'imbriquer la partie BD dans un TransactionScope spécifique. Simplement parce que sans cela, nous obtenons un message d'erreur particulièrement abscons indiquant un problème d'enrôlement dans la transaction ambiante. La seule solution possible a été de mettre en place ce contournement. J'ai lu un post Microsoft sur le sujet que ce bug serait corrigé avec le SP1 de Vista et bien sûr sur Windows Server 2008 RTM. Qui vivra verra ...
- La génération du proxy WCF
Histoire de voir les améliorations Visual Studio 2008 par rapport aux extensions Orcas de Visual Studio 2005 bloquées en CTP / novembre 2006, je vous propose d'utiliser l'assistant permettant de générer automatiquement les proxies clients. Et là, tout de suite je peux vous dire que c'est pas mal du tout ! J'en veux pour preuve cette capture d'écran assez intéressante :
 Visual Studio 2008 : Add Service Reference
La génération du proxy se charge aussi du paramétrage du fichier de configuration. J'ai simplement retouché les parties concernant la sécurité ainsi que les quotas et les tailles des messages. Ce qui nous donne :
 Visual Studio 2008 : Fichier de configuration généré
On regrettera simplement qu'à chaque mise à jour du proxy (pour répercuter les modifications sur les contrats de service par exemple), l'assistant génère de nouvelles clés pour les EndPoint, les Behaviors. Mais en y réfléchissant bien on se rend compte que cela permet d'éviter d'écraser les éventuelles modifications faites à la main !
- La partie TxF
Cette partie est complètement concentrée dans la classe « FileGenerationService » dont voici le diagramme :
 Diagramme de classe FileGenerationService
En fait, pour bien comprendre le fonctionnement, il faut comprendre que tout est basé sur l'API Win32 CreateFileTransacted(...) dont voici la définition : [DllImport("Kernel32.Dll", EntryPoint = "CreateFileTransacted", CharSet = CharSet.Unicode, SetLastError = true)] private static extern SafeFileHandle CreateFileTransacted( [In] String lpFileName, [In] SafeTransactionHandle.FileAccess dwDesiredAccess, [In] SafeTransactionHandle.FileShare dwShareMode, [In] IntPtr lpSecurityAttributes, [In] SafeTransactionHandle.FileMode dwCreationDisposition, [In] int dwFlagsAndAttributes, [In] IntPtr hTemplateFile, [In] SafeTransactionHandle txHandle, [In] IntPtr miniVersion, [In] IntPtr extendedOpenInformation ); C'est cette API qui tire tout le reste ! En effet, elle nous demande de connaître ce qu'est un « SafeFileHandle », contenu dans l'espace de nom « Microsoft.Win32.SafeHandles » (c'est un wrapper vers un handle de fichier). Ensuite, en dehors de quelques paramètres simples (code hexadécimaux pour exprimer le type d'accès, le verrouillage, etc.), il ne reste plus qu'un type « inconnu » : « SafeTransactionHandle ». Il s'agit d'un wrapper vers un handle de type transactionnel utilisé par TxF offrant des services permettant de gérer (fermer, libérer) le handle sous-jacent.
Ce « SafeTransactionHandle » tire « IKernelTransaction », permettant de récupérer une transaction au niveau du noyau par l'utilisation de la classe « TransactionInterop ». Ensuite, c'est à partir de cette référence que l'on peut récupérer ledit handle transactionnel via l'appel à la méthode « GetHandle ». C'est en dernier lieu ce handle transactionnel que l'on passera à l'appel de « CreateFileTransacted ».
Bref, on est dans le Plateform/Invoke jusqu'au cou et même plus ! Ensuite cela, devient plus trivial car on se sert du handle obtenu au travers d'un simple StreamWriter.
Voici l'utilisation complète :
 Code de la génération transactionnelle du fichier
Cette partie de l'article est basée sur le post suivant et a un peu été revu et amélioré en fonction de ce que je souhaitais faire concrètement.
Voici à quoi ressemble l'interface du processus porteur :
 Le processus porteur
Rien à dire sur le processus porteur en dehors du fait qu'en production on n'utilise pas une application console ! Le code du processus porteur est assez générique puisque je me suis débrouillé pour que l'ensemble des services déclarés dans le fichier de configuration soient chargés automatiquement, moyennant une petite astuce sur le nommage, tel que nous allons le voir. Le fichier de configuration du processus porteur va définir deux choses : la chaine de connexion à la base et la liste des services hébergés.
 Fichier de configuration du processus porteur
J'avoue avoir pris goût à l'utilisation l'UAC (User Account Control) sur Vista, mais la ligne de commande qui va suivre ne va pas m'aider à vous en convaincre ! Mais bon, je suis beaucoup plus serein d'un point de vue sécurité maintenant qu'avant. Contraignez-vous à l'utiliser pendant une semaine et vous verrez que vous apprendrez plein de choses intéressantes sur Vista et la sécurisation d'un OS. L'essayez, c'est l'adopter ! J'ai donc mis en place les métadonnées sur un port différent du port 80 et il faut l'autoriser sur http pour un utilisateur d'un domaine Windows. D'où les étapes suivantes pour le faire : - Ouvrir une fenêtre de commande DOS en temps qu'administrateur local
- Lancer la commande suivante avec les bons paramètres :
netsh http add urlacl url=http://+:80/MyUri user=DOMAIN\user Ce qui a donné concrètement pour moi : netsh http add urlacl url=http://+:1111/Metadata user=BEWISE\fcolin A partir de là, il reste à lancer le host. Pour cela, il faut dans un premier temps paramétrer « DataDirectory » (cf. fichier de configuration) pour spécifier le chemin fixe au fichier mdf que je souhaitais conserver dans la racine du projet. C'est pourquoi je calcule un chemin absolu à partir du répertoire d'exécution courant et j'affecte ensuite la variable de domaine correspondante : AppDomain.CurrentDomain.SetData("DataDirectory", System.IO.Path.GetFullPath(@"..\..\")); Comme indiqué précédemment, j'ai souhaité mettre en place un chargement dynamique des services décrits dans le fichier de configuration. Malheureusement, la syntaxe au niveau d'un tag « service » ne permet pas de spécifier l'assembly contenant le service, ce qui est problématique pour le charger puisqu'il faut en connaitre le type pour démarrer l'écoute WCF, tout est dit. Pour résoudre cette problématique, on pouvait par exemple associer en configuration le type et l'assembly à charger pour y avoir accès. Cette solution m'ennuyait car elle supposait que l'on maintienne à jour deux configurations : l'une pour l'infrastructure WCF et l'autre pour le paramétrage des assemblies associées aux types. J'ai donc opté pour une solution basée sur une règle de nommage : X.Y.Z où X.Y représente le nom de l'assembly (sans .dll) contenant le type et Z le nom du type qui m'intéresse et cette information, nous l'avons dans le tag « <service name=""> ». La boucle est bouclée, le reste n'est qu'un peu de parcours d'un fichier de configuration Xml et un peu réflexion !
 Code de démarrage des hosts WCF
Le contrat de données d'un employé a été marqué classiquement avec les attributs WCF qui vont bien (DataContract et DataMember). Mais cela n'a pas suffit du fait de l'utilisation de Linq To SQL. J'ai donc complété par l'attribut « Column » spécifique à Linq To SQL. A noter qu'il n'y a pas d'incidence sur la notion de contrat WCF en tant que tel. Il s'agit juste d'une décoration supplémentaire afin de gérer au mieux l'utilisation de Linq To SQL. Voici un échantillon du résultat obtenu :
 Contrat de données
Vous noterez aussi l'utilisation des propriétés auto-implémentées (pas de déclaration de membres privés) dans cette classe.
Le contrat de service d'un employé définit les services suivants :
 Le Contrat de service
Puisqu'une les classes implémentant ce service interviendront dans un flot transactionnel démarré, une session est obligatoire au niveau du host. Notez aussi, l'attribut « TransactionFlow » qui indique que le mode flot transactionnel est obligatoire (notion qui permet de s'enrôler dans une transaction qui sera démarré du côté du client du composant).
Le service implémente le contrat de service précédemment défini. Il n'y a aucune intelligence particulière dans cette implémentation du fait que toute la partie requêtage est réalisée dans une sous-couche technique. Pour les deux méthodes transactionnelles, nous définissons les comportements suivant par décoration : [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)] Une transaction doit déjà avoir été définie avant l'appel des méthodes et ces dernières valideront automatiquement la transaction dès leur sortie et si aucune exception n'a été déclenchée. A noter la définition sur la classe d'un mode de concurrence de type « Single » n'autorisant qu'un seul thread à un instant donné, ce qui représente le comportement par défaut de WCF. Voici un exemple d'utilisation de la couche technique d'accès aux données qui se veut générique : public void Delete(Employee emp) { Sample.Data.Table<Employee> instance = new Sample.Data.Table<Employee>(); instance.Delete(emp); } public List<Sample.DataContracts.Employee> GetAll(int? employeeID) { Sample.Data.Table<Employee> instance = new Sample.Data.Table<Employee>(); if (!employeeID.HasValue) return instance.GetTable(); else return instance.GetTable(c => c.EmployeeID == employeeID); } Vous noterez l'utilisation d'une expression lambda afin de réaliser un filtrage sur l'identifiant d'un employé. Cette syntaxe est très pratique puisqu'elle permet d'avoir une couche technique complètement générique tout en permettant des conditions de filtrage spécifiques écrites de manière très simples.
Cette couche définit une classe générique unique permettant de gérer les opérations de base sur une table une base de données en s'appuyant et en encapsulant complètement Linq To SQL.
 Diagramme de classe de la couche d'accès aux données
Voici le code de la classe Table<T> :
 Code de la classe d'accès aux données – Partie 1
 Code de la classe d'accès aux données – Partie 2
On se rend bien compte de la simplicité du requêtage qu'il soit en ajout ou en suppression. Le DataContext offre encore bien plus de fonctionnalités avec notamment la gestion des procédures stockées, les relations 1-1, 1-N.
J'ai volontairement mis de côté certains aspects dans cet article : - les aspects sécurité avec WCF. Bien évidemment, c'est mal mais ce n'était pas l'objet de ma prose. Histoire de ne pas vous laisser dans l'expectative, voici une référence intéressante sur les notions de sécurité avec WCF et dont je vous recommande la lecture. Il s'agit d'un article de Stéphane Goudeau intitulé « Windows Communication Foundation : Sécurité. Mise en perspective des patterns de sécurité offerts par WCF ».
- Vous le remarquerez le code de la partie IHM n'est pas très poussée et nécessiterait quelques améliorations !
- Je n'ai pas non plus géré les problématiques de concurrence des mises à jour et des suppressions
- Pour tester l'application, il suffit
- Créer de nouveaux employés
- Lors de la mise à jour de l'image sur la bonne ligne, sélectionnez ladite ligne en cliquant dans une de ses cellules
- Valider la transaction
- Cocher les cases de suppression de certains nouvellement ajoutés
- Ajouter de nouveaux employés
- Valider la transaction
- Vérifier la génération d'un fichier XML dans le répertoire d'exécution
- Pour faire « planter » la transaction ambiante, il suffit de choisir un employé déjà existant en BD et référencé dans une autre table, de répéter les manipulations précédentes et de vérifier la récupération d'une exception de type contrainte référentielle et de vérifier que la génération du fichier n'a pas abouti.
|
|
|
|
|