Introduction
Jouons un peu avec WCF.
Aujourd'hui, but de la manoeuvre : Piloter depuis une application (cliente) l'interface graphique d'une autre application (serveur).
Tout d'abord, il est important de comprendre les concepts de base:
Un service WCF sera composé d'une interface. Cette interface établira le contrat que le service doit absolument respecter. Puis vient le service lui-même qui implémentera l'interface et ainsi, respecter son contrat.
Jusque là, rien de bien compliqué. Visual Studio 2008 permet même de créer un projet de type "Service WCF".
Par défaut, lors de la création d'un service WCF, l'interface et la classe d'implémentation sont créés dans le même projet. Ceci n'est pas toujours la configuration idéale, nous verrons pourquoi par la suite.
Pour notre exemple, nous allons avoir besoin de deux projets de type application: L'un affichant une interface graphique, et la seconde, en ligne de commande, permettant d'intéragir avec la première. Les interactions se feront au moyen d'un service WCF qui sera défini par un troisième projet de type Class Library, et qui pourra êter créé à partir du Wizard de nouveaux projets en choisissant un projet de type WCF Service. Nous considèrerons l'application graphique comme étant le serveur WCF, l'application en ligne de commande comme le client WCF, et le troisième projet comme étant le Contrat définissant le comportement du service. Remarquez que le serveur WCF opèrera au sein d'une application tout à fait standard, et non pas sur IIS. (D'où le Self-Hosted du titre de ce post)
Il est impératif que les deux parties aient connaissance de la nature du service qui leur permettront de communiquer. Comme nous utilisons WCF, il existe plusieurs approches pour obtenir le même résultat. Concernant le serveur, il n'y a qu'une seule façon de faire les choses correctement : Il s'agit de référencer le projet de Contrat, puis d'y implémenter l'interface qu'il contient. Concernant le client, voici les deux façons que nous avons retenues : La première consiste à référencer une assembly compilée. La seconde se résume à n'ajouter dans le projet client qu'une référence sur le fichier source de l'interface se trouvant dans le projet de Contrat, grâce au menu Add Existing Item, puis en choisissant Add as Link dans la boîte de dialogue qui apparaît.
Pour ce qui est des avantages et des inconvénients de chacune de ces solutions, nous aborderons le sujet en détails à la fin de cet article.
Le Contrat
La première chose à faire est de spécifier le comportement du contrat qui définira le service WCF. Implémentons alors l'interface de notre service. Par défaut, dans un nouveau projet de type service WCF ce fichier se nomme IService1. Bien sûr tout le monde a ses petites habitudes. Pour ma part, ma convention de nommage est la suivante : Concernant l'interface - la définition proprement dite du contrat - il s'agira de IxxxContract. Pour ce qui est de l'implémentation de cette interface, elle se trouvera dans une classe nommée XxxContract, les xxx représentant bien évidemment un libellé distinctif.
Il existe une multitude d'options concernant la façon dont se comportera un service WCF face à des connexions simultannées, la possibilité de gérer des requêtes statefull, ou encore la gestion du multithreading. Nous ne nous attarderons pas sur ces comportements. nous utiliserons donc la plupart des options par défaut. La MSDN présente un complément d'information complet à ce sujet.
IxxxContract
Cette interface se présente comme une interface C# tout à fait standard. Il s'agit donc de définir les méthodes et les propriétés à respecter pour toutes ses implémentations. WCF nécessite tout de même certaines informations supplémentaires pour connaître quelles seront les méthodes que le serveur pourra exposer au client, et pour identifier le contrat du service. Ceci s'effectue au moyen d'attributs (ServiceContractAttribute et OperationContractAttribute) Le premier identifiera une interface comme étant un contrat de service, alors que le second permettra d'indiquer à WCF quelles méthodes doivent être considérées comme Opérations du service. Les opérations ne sont ni plus ni moins que les méthodes d'un service qui seront consommables par un client. Il est dès lors possible de forcer une implémentation du service à contenir une méthode sans qu'elle ne soit exposée au client en ommettant simplement l'attribut OperationContractAttribute.
[ServiceContract]
public interface IxxxContract
{
// TODO: Add your service operations here
[OperationContract]
void MyFirstOperation(MyCompositeObject myObject);
}
Nous pouvons remarquer que nous utilisons ici un objet composite comme paramètre d'entrée de l'opération MyFirstOperation. Cet objet doit également être connu par le client ainsi que le serveur. Il est donc nécessaire de définir le contrat qui régit cet objet. Ajoutons donc la classe suivante :
[DataContract]
public class MyCompositeObject
{
[DataMember]
public string Foo{ get; set; }
[DataMember]
public DateTime Bar{ get; set; }
}
DataContractAttribute Définit un contrat de donnée : Il permet d'identifier la classe en question comme un objet, qui devra être sérialisable, afin d'être utilisable dans un service WCF. Cet objet contiendra donc un ou plusieurs champs qui seront exposés ou non par le service. Afin de définir quels champs devront être exposés, il convient de les marquer à l'aide de l'attribut DataMemberAttribute.
Notre contrat est à présent clairement défini. Passons à présent à l'implémentation du corps des opérations.
XxxContract
Cette classe représente l'implémentation du service lui-même. Elle se trouve par défaut dans le même projet que l'interface IxxxContract, mais peut très bien se trouver dans un autre projet, du moment que l'interface qu'elle implémente peut être résolue en ajoutant les références adéquates. Une raison pour séparer la classe de son interface pourrait d'imaginer plusieurs implémentations différentes. Par exemple, une version Self-Hosted et une auter implémentation pour une utilisation Web-based du service WCF. L'implémentation serait alors différente, et il incomberait à chaque hôte du service d'implémenter sa propre logique métier en respectant le contrat établi. Il peut également être plus agréable, pour des raisons moins nobles, de préférer implémenter le service dans le projet Serveur pour éviter des problèmes de références circulaires. Dans notre exemple, le projet serveur référence le projet de Contrat qui, s'il veut pouvoir modifier un élément de l'interface graphique du serveur, doit en posséder une référence. Ceci n'est malheureusement pas possible, car nous nous trouverions face à une référence circulaire.
Afin de palier à ce problème, il existe deux possibilités. La première, la moins élégante, serait de déplacer la classe d'implémentation du service dans le projet Serveur. La seconde, celle que nous utiliserons ici, consiste à créer un évènement dans la classe d'implémentation. De cette façon, dès qu'une opération devant manipuler un élément du projet serveur est appelée, elle lèvera un évènement en fournissant les éventuelles informations nécessaires via un objet dérivé de la classe EventArgs.
Nous voyons donc que cette solution nous impose de créer un delegate, un event, et une nouvelle classe représentant les arguments à passer à l'évènement en question. L'avantage est que le code est mieux découplé, plus flexible, et permet d'accrocher plusieurs delegates en réponse à un seul évènement. Il faudra bien évidemment veiller à s'abonner à l'évènement du côté serveur, et ce, dès l'hébergement du service WCF.
Une note à ce propos est que si le service est exécuté en mode Singleton, il sera possible de récupérer l'instance du service grâce à la propriété SingletonInstance de la classe ServiceHost. Dans le cas contraire, notre évènement devra être statique, afin de pouvoir être le même pour toutes les instances du service hébergé.
Le Client
Nous allons poursuivre à présent avec l'implémentation du client WCF. Un client WCF est généralement composé de deux parties fondamentales. La consommation du service, et sa configuration.
Si la consommation d'un service WCF se présente comme une application C# habituelle, la configuration du service introduit un concept jusqu'alors relativement marginal. Il s'agit d'un fichier xml accompagnant l'exécutable, et qui se chargera de fournir à l'application les données relatives à l'adresse de l'hôte WCF, la manière dont la couche de transport est gérée, et le contrat à utiliser pour le service WCF à consommer. Il est à noter qu'un tel fichier de configuration est également présent du côté serveur, mais avec un contenu légèrement différent, pour définir les besoins spécifiques à l'hébergement du service WCF.
Consommation
La consommation d'un service WPF se fait relativement facilement. Le concept est de créer un canal menant au service hébergé. Pour ce faire, nous utiliserons une usine à cannaux -ChannelFactory<T>. La première étape est de créer une ChannelFactory. Il s'agit là d'une classe générique, nous pourrons donc choisir sur quel contrat seront créés les cannaux fabriqués par l'usine.
ChannelFactory<IxxxContract> factory = new ChannelFactory<IxxxContract>("xxxEndpoint");
IxxxContract client = factory.CreateChannel();
Remarquons le paramètre du constructeur de ChannelFactory<IxxxContract> : Cette chaîne de texte identifie un endpoint. L'usine ainsi créée fabriquera des cannaux visant le endpoint spécifié. La définition du endpoint en question sera détaillée dans le prochain paragraphe, Configuration.
Après avoir appellé la méthode CreateChannel() de notre usine, nous obtenons une instance de IxxxContract, ou plus précisément, une instance de la classe d'implémentation du service hébergé sur l'adresse désignée par le endpoint.
cette instance peut alors être utilisée pour appeller les opérations du service WCF, comme l'on appellerait de simples méthodes sur un objet local.
MyCompositeObject myObject= new MyCompositeObject ();
client.MyFirstOperation(myObject);
Voyons à présent comment se présente ce fameux endpoint.
Configuration
Il s'agit là d'un simple fichier XML, généralement nommé App.config pour des applications Windows, ou Web.config pour des applications web. Il peut être composé de diverses sections, en particulier pour le cas des applications web, qui utilisent ce système depuis les débuts d'ASP.Net pour une multitude de paramètres, comme des connectionstrings de base de données par exemple.
Pour notre application client, le fichier App.config sera relativement succint. Les points important sont concentrés dans le tag endpoint.
<system.serviceModel>
<client>
<endpoint name="xxxEndpoint" address="http://localhost:2008/ArbitraryUserFriendlyName" binding="wsHttpBinding" contract="IxxxContract" />
</client>
</system.serviceModel>
La propriété name permet de nommer notre endpoint afin de pouvoir le référencer lors de la création d'une ChannelFactory. Nous retrouvons donc ici le même libellé que celui fourni au constructeur de notre usine à cannaux.
la propriété address identifie l'adresse du serveur chargé d'héberger le service.
la propriété binding indique la façon dont sera gérée la couche de transport. Dans noter cas, nous allons communiquer avec le serveur à la façon d'un WebService en passant par le protocole HTTP.
et finalement, la propriété contract indiquera à la framework quel est le contrat à utiliser pour ce service.
Notons qu'il est possible d'implémenter et de tester le client avant le serveur, car Visual Studio permet d'héberger temporairement le service WCF, s'il est référencé en tant que tel par le projet Client.
Le Serveur
Tout comme le projet client, le projet serveur est composé de deux parties fondamentales qui sont cette fois-ci l'hébergement et la configuration du service.
Hébergement
L'hébergement s'effectue de façon très simple. La classe ServiceHost permet d'encapsuler un service, et de l'héberger grâce à sa méthode Open() en étant totalement transparent quant à la gestion des connexions et les instances concurrentes du service.
ServiceHost sh = new ServiceHost(typeof(XxxContract));
sh.Open();
Configuration
Comme pour le client, il s'agit d'un fichier au format XML. les informations importantes se trouvent cette fois-ci dans les tags service et endpoint.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="XxxContract">
<endpoint address="http://localhost:2008/ArbitraryUserFriendlyName" binding="wsHttpBinding" contract="xxxNameSpacexxx.IxxxContract" />
</service>
</services>
</system.serviceModel>
</configuration>
Par le biais du tag service et de sa propriété name, il nous est possible de définir la classe d'implémentation du service à utiliser.
Concernant le tag endpoint, ils ont la même vocation que pour leur homologue du côté client.
Notons tout de même qu'il est possible de définir un chemin complet en spécifiant le namespace en plus du nom d'une classe ou d'une interface, comme ici, dans la propriété contract du endpoint.
Assemblies or Linked Items - Pros & Cons
Nous avons vu qu'il existe deux façons pour un client WCF de référencer le contrat d'un service. L'une référence une assembly compilée, l'autre référence un code source. Les conséquences qui en découlent sont les suivantes :
Maintenance
Pour ce qui est de la maintenance, il est préférable d'utiliser une référence sur une assembly binaire. En effet, si le projet venait à être séparé et développé par deux personnes différentes, les fichiers ajoutés en tant que liens pointeraient sur un lien brisé. Il serait alors nécessaire d'utiliser un logiciel de versionning, mais surtout, de maintenir à jour non seulement le projet Client, mais également le projet Contrat.
Références Additionnelles
Si comme dans notre cas, le projet Client ne contient pas toutes les assemblies permettant la sérialisation d'un contrat WCF, il sera nécessaire de les ajouter dans le cas de figure d'une référence sur la source de l'interface. Au contraire, dans le cas d'une référence sur l'assembly du projet Contrat, celle-ci étant déjà compilée, elle contient déjà tout ce qui est nécessaire à la sérialisation des messages du service.
Dès lors, pourquoi opter pour la solution d'une référence sur la source de l'interface, si elle apporte tous ces inconvénients ?
Nombre d'assemblies réduit
En référençant l'interface du service dans le client sous forme de lien sur sa source, il devient alors possible de s'affranchir du projet Contrat, et d'intégrer directement l'interface dans le projet Serveur, éliminant ainsi la nécessité t'avoir une dll supplémentaire tant du côté client que du côté serveur. Cependant, comme toute librairie compilée de façon statique (car c'est bien de celà qu'il s'agit) la maintenance est moins flexible dans le sens où si une modification intervient sur le contrat, il sera alors nécessaire de recompiler les deux applications alors que dans une configuration avec trois projets, le client n'aura pas à être recompilé. Seule sa librairie de Contrat devra être remplacée.
Il est donc fortement conseillé d'opter pour une référence sur l'assembly de Contrat.
Conclusion
C'est ici que s'achève mon premier post technique. J'ai essayé de retranscrire ici mes premiers pas en WCF. Cette technologie semble très prommetteuse, notamment concernant la possibilité de configurer le mode de transport simplement en modifiant un fichier de configuration xml. Il devient alors possible d'imaginer des applications ayant le même code mais optimisées pour différentes applications, par exemple, on pourrait imaginer dans notre cas, faire une application amenée à n'être exécutée qu'en local et éviter tout l'overhead des connexions TCP/IP et du protocole HTTP en utilisant des named pipes, sans recompiler nos applications, simplement en changeant le binding des endpoints définis dans les deux fichiers XML.
8066a1a8-78cf-4057-98fa-a2d7c97adc64|0|.0