L’objectif de cet article est de présenter la mise en place d’une transaction simple avec Windows Communication Foundation faisant intervenir deux « Resources Managers » différents : une base de données SQL Server 2005 et le système de gestion de fichier de Vista.
La lecture de cet article suppose quelques connaissances de base sur Windows Communication Foundation ainsi qu’une bonne connaissance du langage C#.
L’exemple a été développé en C# avec Visual Studio Team System 2008, Framework 3.5 sur Windows Vista Ultimate US.
Dans le monde .NET, les transactions respectent les caractéristiques de base classiques à savoir : Atomicité, Cohérence, Isolation et Durabilité. Au sein d’une application .NET donnée, les transactions peuvent être gérées à différent niveaux :
- Au niveau du « Resource Manager » directement (un SGBD par exemple)
- Au niveau ADO.NET
- Au niveau COM+
- En utilisant « System.Transaction »
Etant entendu que c’est en définitive à la charge de chaque « Resource Manager » de fournir son propre mécanisme de compensation en cas d’échec d’une étape de la transaction pour assurer l’annulation des actions le concernant.
Vous imaginez facilement, que plus la transaction sera pilotée proche du SGBD, plus elle sera performante et par conséquent … propriétaire. D’un autre côté, plus l’on s’éloigne du « Resource Manager », plus il sera facile pour la transaction de « s’enrôler » dans une transaction ambiante gérée à un autre niveau : via Microsoft Distributed Transaction Coordinator, le service MSDTC bien connu des systèmes d’exploitation Windows.
Bref, le choix de la méthode est fonction de critères discriminants tels que :
- Besoin d’interagir avec des « Resources Managers » hétérogènes distribués ou non
- Le besoin précédent est-il ponctuel ou systématique
- Les transactions sont-elles gérées dans le corps d’une seule méthode ou réparties sur différentes méthodes de classes.
Pour ma part, je choisis souvent la voie du milieu bien connue des bouddhistes : ni trop, ni trop peu. Et il est vrai qu’avec l’arrivée de « System.Transaction », le choix devient plus simple à faire. Pour mettre en œuvre lesdites transactions de cet exemple, nous utiliserons l’espace de nommage « System.Transaction » disponible depuis le Framework 2.0.
Comme je le disais précédemment, cet espace de nom au travers de sa classe « TransactionScope » représente un bon compromis. En effet, il s’agit d’un système intelligent hybride qui utilise par défaut une transaction locale et qui peut utiliser une transaction distribuée supportée par MSDTC. Ce système de gestion de transaction est donc capable de promouvoir une transaction dite « simple » vers une transaction distribuée si une des conditions suivantes est remplie :
- Utilisation d’objets différents faisant intervenir de nouveaux contextes transactionnels
- Transport du flot d’exécution vers un autre « AppDomain »
- Interaction avec une un autre « Resource Manager »
Il va de soit que le temps de promotion d’une transaction simple vers une transaction distribuée a un coût sur les performances générales de la transaction (le temps de la promotion justement), mais c’est le prix de la généricité dans ce cas.
Quelques particularités importantes :
- L’instance de la classe « TransactionScope » est souvent utilisée via un bloc « Using » chargé de la libération des ressources (TransactionScope est « IDisposable »)
- Dans ce cadre là, il n’est pas utile d’annuler (et impossible car la méthode n’existe pas) la transaction dans le bloc. Le non-appel à la méthode « Complete » et la sortie du bloc « using » suffisent
- Le TimeOut d’exécution est de 60 secondes
- Possibilité d’imbriquer les Scope transactionnels les uns avec les autres
- Possibilité d’abonnement à deux événements intéressants
- System.Transactions.Transaction.Current.TransactionCompleted : transaction validée
- System.Transactions.TransactionManager.DistributedTransactionStarted : promotion d’une transaction simple vers une transaction distribuée. Dans ce cas, il faut bien faire attention au fait que la transaction est promue après le traitement de l’événement.
Voici un exemple d’utilisation :
using (TransactionScope scope = new TransactionScope())
{
try
{
using (SqlConnection myCn1 = new SqlConnection("..."))
{
SqlCommand cmd = new SqlCommand("Insert …", myCn1);
cmd.CommandType = CommandType.Text;
myCn1.Open();
cmd.ExecuteNonQuery();
}
using (SqlConnection myCn2 = new SqlConnection("..."))
{
SqlCommand cmd = new SqlCommand("Delete …", myCn2);
cmd.CommandType = CommandType.Text;
myCn2.Open();
cmd.ExecuteNonQuery();
}
scope.Complete();
}
catch (Exception ex) { … }
finally { … }
}
On se rend bien compte de l’apport de transparence à l’utilisation d’un « TransactionScope ». Par la suite, il est possible d’accéder aux propriétés de la transaction ambiante via « System.Transactions.Transaction.Current ».
Le fonctionnement d’un gestionnaire de ressources distribuées tel que Microsoft Distributed Transaction Coordinator (MSDTC) est le suivant :
- L’application appelle le Transaction Manager pour créer la transaction (gestion du contexte transactionnel, de l’atomicité et de la durabilité)
- Tous les composants intervenants sont enrôlés dans la transaction ambiante
- Le Transaction Manager garde de manière séquentielle un log afin que la décision finale de validation ou d'invalidation soit durable et garantie
- Au final, si tous les RM impliqués sont prêts alors la transaction est validée et le log est effacé
- Sinon le Transaction Manager diffuse un ordre d’annulation à tous les « Resources Managers » impliqués
Il faut savoir que WCF supporte les transactions « natives » et « WS-Atomic » (standard). Ce dernier protocole définit comment implémenter des transactions sur la base d’un commit à deux phases :
- Phase 1
- Le coordinateur demande à chaque « Resource Manager » (RM) de se préparer au commit
- Chaque RM vote pour valider ou invalider la transaction
- Le coordinateur collecte les votes et prend une décision (commit ou rollback)
- Phase 2
- Le coordinateur demande à chaque RM de valider ou d’invalider
- Si un RM doit valider, il accuse réception de l’achèvement ou bien réalise le rollback
- Le coordinateur attend les AR des RM pour indiquer que la transaction s’est terminée correctement
Ce mécanisme permet donc d’implémenter des transactions de type « tout ou rien » avec des systèmes faiblement couplés dispersés sur différentes plateformes. WCF fournit une implémentation de WS-AT construite sur MSDTC. Il est possible de configurer WS-AT via les propriétés du DTC. Pour cela, il faut registrer la dll suivante pour faire apparaitre l’onglet qui va bien !
- Regasm.exe /codebase wsatui.dll (« C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin »)
Finalement, au beau milieu de la réalisation de l’article, je me suis dit qu’il serait dommage de réaliser la couche l’accès aux données avec le Fx 3.5 sans utiliser Linq To SQL. Je m’y suis donc attelé afin de mettre en place 3 choses : la sélection éventuellement filtrée, l’ajout et la suppression.
Linq To Sql est une implémentation d’un petit mapper objet-relationnel basé sur ADO.NET, intégré à Visual Studio 2008 et ciblant pour l’instant exclusivement les bases SQL Server. Il permet donc de faire la liaison entre des classes et des tables en base de données de manière quasi transparente pour le développeur. Je dis quasi transparente, car il faudra quand même décrire le mapping existant soit à la main sous la forme d’un fichier de mapping ou bien avec des attributs, soit en utilisant le Designer intégré à Visual Studio 2008. Voici un exemple de requêtage que j’ai pu utiliser au travers d’une classe générique :
public List<T> GetTable(Func<T, Boolean> predicate)
{
DataContext dc =
new DataContext(
ConfigurationManager.ConnectionStrings["NorthwindConnectionString"]
.ConnectionString,
new System.Data.Linq.Mapping.AttributeMappingSource()
);
return (
(from c in dc.GetTable<T>()
select c).Where(predicate).ToList()
);
}
Je reviendrai techniquement sur cette partie un peu plus loin dans cet article. Ce que j’apprécie particulièrement dans l’utilisation de Linq To Sql et de la syntaxe Linq en général c’est que l’on décrit précisément ce que l’on veut obtenir, mais pas comment faire pour l’obtenir. C’est donc l’implémentation sous-jacente de Linq qui se charge du comment (Linq To Object, Linq To Xml, Linq To Sql, etc.) contrairement à ce que l’on faisait avant en ouvrant précisément une connexion et en exécutant tel ordre SQL par exemple. L’idée d’apporter intrinsèquement et nativement au langage le requêtage sans passer par un Framework compliqué est brillante.
Vous trouverez sur le Web de nombreuses explications sur l’utilisation de Linq To Sql et je ne m’étendrai pas davantage là-dessus. Je vous recommande donc le plan suivant pour une mise en jambe rapide :
Une chose était importante pour moi dans cet exemple : le fait de pouvoir utiliser mes propres définitions de contrat de données et non pas celles générées par le Designer Graphique de Linq To Sql. Or, le Designer Linq To SQL ne permet pas cette séparation stricte. J’ai donc du réaliser cela à la main et j’ai choisi la décoration par attribut à la définition d’un fichier de mapping car plus simple à faire à la main.
Non non, il ne s’agit pas d’une marque de motos bien connue des spécialistes, mais d’un service nouvellement offert par les plateformes Windows Vista et bientôt Windows Server 2008 seules. Il permet de rendre les transactions disponibles au niveau des composants systèmes tels que NTFS (Transactional NTFS ou TxF) et la base de registres (Transactional Registry ou TxR).
Le KTM est aussi dépendant d’une autre API Système, le Common Log File System (CLFS) assurant la gestion des logs, rendant ainsi possible une gestion transactionnelle. Dans le cadre de cet article, nous travaillerons seulement sur TxF. TxF permet ainsi aux opérations de gestion de fichiers sur une partition NTFS de participer à une transaction ambiante, qu’elle soit distribuée ou non. Par exemple :
- Mise à jour d’un fichier (contenu et attributs)
- La création d’un fichier et l’insertion en base de données
- La création de fichiers sur plusieurs machines
- Participation à une transaction avec plusieurs autres Resource Managers transactionnels (SGBD, MSMQ)
- Scénario supporté : un écrivain – plusieurs lecteurs concurrents et consistant. Les lecteurs transactionnels voient toujours une version consistante du fichier accédé. Attention, il n’y a pas de support pour les mises à jour concurrentes sur différentes transactions. La conséquence est que TxF n’est pas la solution adéquate pour les scénarios avec des utilisateurs multiples.
Comme il s’agit d’une API que je qualifierai de « bas niveau », cela nécessite de manipuler des structures et des fonctions C. Nous ferons alors beaucoup de P/Invoke. Voici la logique de l’utilisation :
- Création de la transaction ambiante via un TransactionScope
- Récupération d’un « Transacted File Handle » en écriture en lieu et place d’un simple handle
- TxF verrouille l’accès
- Toute opération de modification sur le fichier en dehors de cette transaction recevra une erreur
- Tout lecteur transactionnel sur le fichier recevra une erreur à l‘ouverture dès lors que le fichier est ouvert dans un cadre non transactionnel. Ce tableau issu de la documentation du MSDN résume bien les verrouillages :
|
Transacted |
Non-Transacted |
|
Reader |
Reader/Writer |
Reader |
Writer |
|
Transacted Reader |
Yes |
Yes |
Yes |
No |
|
Transacted Reader/Writer |
Yes |
No |
Yes |
No |
|
Non-Transacted Reader |
Yes |
Yes |
Yes |
Yes |
|
Non-Transacted Reader/Writer |
No |
No |
Yes |
yes |
- En d’autres termes, TxF fournit un niveau d’isolation de type « Read-Committed ». Cela signifie qu’en mode transactionnel :
- Un écrivain unique verra la version la plus récente ainsi que tous les changements réalisés dans le cadre de cette transaction.
- Un lecteur verra seulement la dernière version validée qui existait à l’ouverture du fichier. Le lecteur est donc complètement isolé des modifications des écrivains transactionnels.
- Travail sur le handle transactionnel
- Fermeture du handle transactionnel (avant de faire le commit)
- Validation / invalidation de la transaction ambiante
WCF utilise des « Policy Assertions » afin de contrôler et paramétrer le flot transactionnel. Ces dernières sont spécifiées par des attributs à la fois sur les contrats et les services, mais aussi par configuration.
Les transactions peuvent être initiées par le client ou par le service accédé. Dans le cas d’une transaction initiée par le client, nous utiliserons l’attribut « TransactionFlow » sur le contrat de service afin de préciser la capacité d’un service à accepter une transaction provenant d’un client et à se paramétrer en conséquence. Bien entendu, cette notion n’est pas applicable aux méthodes « OneWay » du fait de leurs natures asynchrones.
[OperationContract()]
[TransactionFlow(TransactionFlowOption.Mandatory)]
void Debiter(Int32 NumCompte, Decimal montant);
D’autres paramètres viennent influencer le comportement transaction d’un service. L’attribut « ServiceBehavior » applicable sur la classe qui implémente les méthodes d’un contrat de service possède des propriétés intéressantes :
- TransactionAutoCompleteOnSessionClose » (false)
- Toute transaction incomplète sera complétée quand la session fermera. Si true, le channel entrant devra gérer les sessions
- « ReleaseServiceInstanceOnTransactionComplete » (true)
- Précise si l’instance du service doit être libérée lorsque la transaction est complétée. Si true, tout nouveau message entraine la création d’une nouvelle instance
- « TransactionIsolationLevel » (Unspecified)
- Spécifie le niveau d’isolation de la transaction au sein du service
- « TransactionTimeout » (60 secondes)
- Durée maximale pendant laquelle la nouvelle transaction doit être complétée
Enfin, l’attribut « OperationBehavior », applicable sur l’implémentation d’une méthode en particulier, permet aussi de paramétrer une transaction via les propriétés :
- Propriété « TransactionScopeRequired » (false)
- Indique si l’opération doit s’exécuter au sein d’une transaction
- Propriété « TransactionAutoComplete » (true)
- Auto-commit de la transaction si l’opération s’exécute avec succès
[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
public int AddNewEmployee(Sample.DataContracts.Employee employee)
Maintenant que nous avons succinctement passé en revue l’ensemble des technologies utilisées dans cet article, analysons un peu le code de la solution.
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 :
Voici la cartographie de la solution. Nous retrouvons un découpage en couche assez classique :
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 :
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 :
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 …
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 :
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 :
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 !
Cette partie est complètement concentrée dans la classe « FileGenerationService » dont voici le diagramme :
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 :
Cette partie de l’article est basée sur le post suivant :
http://www.pluralsight.com/blogs/jimjohn/archive/2006/08/31/36819.aspx 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 :
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.
J’avoue avoir pris goût à l’utilisation de 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’essayer, 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 !
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 :
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 :
[ServiceContract(SessionMode = SessionMode.Required)]
public interface IEmployeeService
{
[OperationContract()]
[TransactionFlow(TransactionFlowOption.Mandatory)]
int AddNewEmployee(Sample.DataContracts.Employee employee);
[OperationContract()]
[TransactionFlow(TransactionFlowOption.Mandatory)]
void Delete(Sample.DataContracts.Employee employee);
[OperationContract()]
System.Collections.Generic.List<Sample.DataContracts.Employee>
GetAll(int? employeeID);
}
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 :
[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
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.
Voici le code de la classe Table<T> :
public class Table<T> where T : class, new()
{
public List<T> GetTable()
{
DataContext dc =
new DataContext(
ConfigurationManager.ConnectionStrings["NorthwindConnectionString"]
.ConnectionString,
new System.Data.Linq.Mapping.AttributeMappingSource()
);
return (
(from c in dc.GetTable<T>()
select c).ToList()
);
}
public List<T> GetTable(Func<T, Boolean> predicate)
{
DataContext dc =
new DataContext(
ConfigurationManager.ConnectionStrings["NorthwindConnectionString"]
.ConnectionString,
new System.Data.Linq.Mapping.AttributeMappingSource()
);
return (
(from c in dc.GetTable<T>()
select c).Where(predicate).ToList()
);
}
public void Delete(T instance)
{
DataContext dc =
new DataContext(
ConfigurationManager.ConnectionStrings["NorthwindConnectionString"]
.ConnectionString,
new System.Data.Linq.Mapping.AttributeMappingSource()
);
System.Data.Linq.Table<T> t = dc.GetTable<T>();
t.Attach(instance, false);
t.DeleteOnSubmit(instance);
dc.SubmitChanges();
}
public void Add(T instance)
{
DataContext dc =
new DataContext(
ConfigurationManager.ConnectionStrings["NorthwindConnectionString"]
.ConnectionString,
new System.Data.Linq.Mapping.AttributeMappingSource()
);
System.Data.Linq.Table<T> t = dc.GetTable<T>();
t.InsertOnSubmit(instance);
dc.SubmitChanges();
}
}
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 » (http://www.netfxfactory.org/docs/wcfsecurity.pdf).
- 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.
J’espère au travers de cet article vous avoir fait partager quelques connaissances autour des transactions WCF par l’utilisation d’un Resource Manager tout nouveau et peu connu : TxF. Bien entendu, les applications de ce genre d’outils (lorsqu’ils seront un poil plus mâtures avec le SP1 Vista et Vindows Server 2008 et successeurs) pourront être relativement intéressantes, pour les installations par exemple.
Bien entendu, on est encore très loin d’une API managée permettant de gérer un Resource Manager de type NTFS, mais nous avons enfin des API bas niveau pour le faire et je dirai que c’est de bon augure pour les OS post Windows Server 2008. Il me tarde quand même cette intégration complète et managée. Mais comme le dit le dicton, « chi va piano va sano ! ».
En dernier lieu, il m’est difficile de terminer cet article sans parler en deux mots des apports du Fx 3.5 sur l’aspect WCF tant ils sont tous intéressants ! Voici donc les principaux qui m’intéressent le plus en ce moment :
- Modèle de programmation http, Sérialisation JSON et intégration ASP.NET AJAX. Ouverture vers un modèle de distribution de composant plus simple que SOAP. Ces deux modèles ne sont pas si antagonistes que cela !
- Syndication : la mise en place d’un flux RSS via un service est grandement facilitée par les nouvelles APIs .NET fournies
- Support des appels à partir d’Assembly « Partially Trusted » (exemple addin Outlook)
Tous ces points forts intéressants nécessiteraient un article pour chacun d’entre eux. Mais c’est une autre histoire et le temps est une chose précieuse et rare en ce moment …