Introduction
Add Web Reference.
La normalisation des différentes spécifications sur lesquelles s'appuie la technologie service web a permis aux éditeurs de produire des outils automatisant leur utilisation. Ainsi, la plateforme Microsoft.NET s'est dotée, dés sa première version, d'outils (« Add Web Reference », « Wsdl.exe ») permettant d'utiliser dans une application.NET des objets et des méthodes fournis par un service Web d'une façon comparable à ce que vous feriez pour vos propres objets.
Mais la simplicité de ces outils ne peut malheureusement faire oublier leurs limites, révélées lors de leur emploi dans un environnement de production industriel et souvent contraignant.
Qui n'a pas pesté contre la génération multiple, au sein de son environnement de développement, d'une même structure de données lors de la génération successive de plusieurs proxys liés à des services web pointant tous, pourtant, sur cette même structure de données ? Vous preniez pourtant bien soin, au sein de la boite de dialogue « Add Web Reference », d'indiquer un espace de nommage identique pour l'ensemble de vos proxys mais vous sous estimiez alors la perfidie de votre environnement de développement qui jouait à renommer vos espaces de nommage en les suffixant d'un nombre entier s'ils avaient déjà été utilisés dans un précédent ajout de référence web.
Loin de vous satisfaire de cette situation, vous avez du soit vous résoudre à vous retrousser vos manches ou soit profiter des trésors dont la toile regorge parfois. Si vous adoriez enfant la chasse au trésor, vous avez surement cru faire fortune suite à la découverte de la librairie publié par Christian Weyer : DynWsLib. Si cette librairie offrait de nombreuses fonctionnalités dont une génération dynamique des proxys de vos services web, elle péchait également au niveau de la capitalisation des structures de données consommées par ces derniers.
Tout vous conduisait à vous dispenser de l'outil « Add Web Reference » et de combiner vos efforts avec différents librairies prises à droite et à gauche sur la toile ou à jongler avec les paramètres offerts par l'outil Wsdl.exe.
La publication de la version 2.0 de la plateforme Microsoft.NET ouvrit de nouvelles perspectives via :
Mais vous veniez à peine de prendre note de ces nouvelles fonctionnalités, qu'une nouvelle interface de programmation Windows pour la communication était annoncée : Windows Communication Foundation (WCF).
ChannelFactory
Cette Api est une des fondations de la version 3.0 de la plateforme Microsoft.NET. Elle sonne le glas de plusieurs technologies de communication dont s'était dotée la plateforme .NET. Mais loin de dramatiser, il nous faut saluer cette initiative qui fédère désormais au sein d'un socle commun toutes les technologies de communication (Web Services, Remoting, MSMQ…) et au sein d'une seule classe toute l'intelligence que l'on peut injecter à un proxy : ChannelFactory<TChannel>.
Ce dernier ainsi doté fait fi de toute génération de code préalable à son utilisation. Il permet de plus via le type paramétré TChannel de spécifier indirectement les structures de données que vous utiliserez lors de la communication avec votre service distant.
Mais que faire en attendant le déploiement massif de cette nouvelle interface de programmation au sein des réseaux d'entreprise ?
La suite de ce document vous présentera une solution permettant de tirer pleinement profit des fonctionnalités de la version 2.0 de la plateforme Microsoft.NET tout en vous préparant à votre future adoption de WCF et donc de réduire vos prochains couts de migration.
Dynamic Proxy Generation
Objectifs et problématiques.
L'objectif est l'implémentation d'un mécanisme offrant une génération dynamique de proxy de service web dont le contrat serait spécifié par une interface. Ce mécanisme doit de plus être développé exclusivement à partir de la plateforme Microsoft.NET 2.0.
Les problématiques sont donc les suivantes :
- Génération dynamique d'un proxy.
- Génération d'un proxy fortement typé.
Ci-dessous, un exemple de code mettant en œuvre le mécanisme souhaité :
ProxyFactory<IWebService> __proxyFactory = new ProxyFactory<IWebService>();
IWebService __proxy = __proxyFactory.CreateProxy();
string __helloWorld = __proxy.HelloWorld();
Génération dynamique d'un proxy
La génération dynamique de proxy avait été solutionnée bien avant la version 2.0 de la plateforme Microsoft.NET. C'est en nombre d'années que nous pouvons compter le moment où Christian Weyer nous faisait part de ses premiers travaux à ce sujet et du lancement du projet : DynWsLib.
Ce dernier a su tirer le meilleur parti de la classe ServiceDescriptionImporter en vue de générer à la volée des proxys au sein d'une librairie sauvegardée dans un répertoire temporaire en vue de leur réutilisation future et donc de ne générer qu'une fois ces derniers.
Limitations
Malheureusement la classe ServiceDescriptionImporter a un comportement par défaut concernant la génération des structures de données identique à celui suivi par les outils présentés en introduction de ce document. Les structures de données qu'elle génère ont un scope d'utilisation réduit au proxy auquel elles sont associées.
Solution
Différentes solutions nous permettent de modifier le comportement par défaut de la classe ServiceDescriptionImporter concernant la génération des structures de données :
- Agir sur le code généré suite à l'appel de la méthode Import de cette classe.
- Utiliser la nouvelle classe SchemaImporterExtension mis à disposition par la plateforme Microsoft.NET dans sa version 2.0.
Notre choix se porte bien entendu sur la seconde alternative, la première pouvant se révéler fort fastidieuse et non raisonnée.
Génération d'un proxy fortement typé.
Comme nous le soulignons dans l'introduction de ce document, il est fort peu aisé en utilisant les outils de génération de proxy de services web mis à disposition par la société Microsoft de spécifier les structures de données que nous souhaitons utiliser pour communiquer avec un service web.
Une nouvelle fois, Microsoft a su réagir et propose désormais un mécanisme au sein de la version 2 .0 de sa plateforme de développement .NET répondant à nos attentes. Il s'agit de développer nos propres gestionnaires de correspondances entre des éléments XML schéma et des classes. Le développement de ces gestionnaires de correspondances personnalisés reposent sur l'implémentation de la classe abstraite SchemaImporterExtension. Nous ne focaliserons dans ce document uniquement sur une des méthodes de cette classe : ImportSchemaType. Le lecteur souhaitant plus de détails quant au fonctionnement de ces gestionnaires de correspondance est invité à lire l'article de Jelle Druyts intitulé « Customizing generated Web Service proxies in Visual Studio 2005 ».
L'implémentation de la méthode ImportSchemaType(string name, string ns, …) nous permettra de lier nos propres structures de données aux différents éléments XML. Le paramètre name indique le nom de l'élément XML schéma dont nous devons établir si nous le souhaitons une correspondance avec un type de notre choix, quant au paramètre ns il nous informe de l'espace de nommage XML associé à cet élément XML. Nous indiquerons en retour de cette méthode le nom complet du type associé à l'élément XML schéma traité.
Pour que vos gestionnaires de correspondance soient utilisés par la plateforme Microsoft.NET, il est nécessaire de les déclarer dans la section schemaImporterExtensions (configuration/system.xml.serialization/schemaImporterExtensions) du fichier de configuration de votre application ou de l'outil devant les utiliser.
Limitations
Cette solution repose sur la déclaration exclusive des gestionnaires de correspondance via l'utilisation d'un fichier de configuration.
De plus, il n'est malheureusement pas possible de paramétrer nos gestionnaires de correspondances lors de leur initialisation puisque celle-ci fait appel à leurs constructeurs par défaut (sans paramètres).
Solution
Il ne nous est pas possible de reposer sur un fichier de configuration en vue de définir notre propre gestionnaire de correspondances. Ce dernier se devra de plus de faire correspondre aux éléments XML, définis dans le document wsdl du service web avec lequel nous désirons communiquer, des types que nous piocherons au sein des librairies référencées par l'interface TProxy.
Paramétrage du gestionnaire de correspondance.
Il nous est impossible de paramétrer notre gestionnaire de correspondance via l'ajout d'un nouveau constructeur. Nous allons alors tirer une première fois partie du support des génériques par la plateforme Microsoft .NET 2.0.
Notre gestionnaire de correspondance sera à l'instar de notre générateur dynamique de proxy paramétré par l'interface TProxy : SchemaImporterExtension<TProxy>. Il pourra ainsi à partir de cette interface parcourir l'ensemble des types définis au sein des librairies référencées en vue de leur faire correspondre des éléments XML.
Lors de cette correspondance, plusieurs paramètres sont à prendre en compte, en effet il est possible de personnaliser la sérialisation XML associée à une classe. Cette sérialisation XML personnalisée doit être supportée, l'unique élément à prendre en compte est la définition d'un espace de nommage XML au niveau du type (via l'attribut XmlRootAttribute) ou bien de l'interface TProxy (via l'attribut WebServiceAttribute).
Nous avons décidé pour des raisons de performance de charger préalablement à l'établissement d'une première correspondance entre un élément XML schéma et un type, l'ensemble des types référencé par l'interface TProxy (TypeExporter) au sein d'un dictionnaire (XmlQualifiedNameTypeCollection) indexé par leur nom ainsi que l'espace de nommage XML s'il le redéfinisse.

Figure 1 - SchemaImporterExtension<TProxy>Diagram class.
Déclaration du gestionnaire de correspondances.
Jusqu'à maintenant marchant dans les pas de la librairie DynWsLib, seule avait été évoquée la méthode Import de la classe ServiceDescriptionImporter pour générer un proxy. Mais cette classe s'est dotée depuis la version 2. 0 de la plateforme Microsoft.NET d'une méthode statique GenerateWebReferences répondant à la même problématique. Mais l'avantage de cette deuxième méthode est qu'elle permet de déclarer programmatiquement des gestionnaires de correspondances. La limite soulevée précédemment quant à l'utilisation de fichier de configuration n'est donc plus d'actualité.
La déclaration d'un gestionnaire de correspondances s'appuie sur la classe WebReferenceOptions et la propriété SchemaImporterExtensions. Cette propriété est une collection de chaines de caractères, il vous suffit d'ajouter à cette collection le nom qualifié d'assembly du type de votre gestionnaire de correspondance pour qu'il soit utilisé lors de l'appel à la méthode GenerateWebReferences. Cette méthode prenant en paramètre une instance de la classe WebReferenceOptions.
// Discover the wsdl document and its references.
DiscoveryClientProtocol __discoveryClientProtocol = new DiscoveryClientProtocol();
__discoveryClientProtocol.DiscoverAny([Wsdl]);
// Create a namespace and a unit for proxy client compilation.
CodeNamespace __namespace = new CodeNamespace();
CodeCompileUnit __cplUnit = new CodeCompileUnit();
__cplUnit.Namespaces.Add(__namespace);
// Create a web reference using the WSDL collection.
// The discovery documents are associated with the previously created code namespace.
WebReference __webReference = new WebReference(__discoveryClientProtocol.Documents, __namespace);
// Create a web reference collection which includes the previously instancied WebReference object.
WebReferenceCollection __wbRefs = new WebReferenceCollection();
__wbRefs.Add(__webReference);
// Define options for the compilation of the proxy client.
CodeDomProvider __csharp = CodeDomProvider.CreateProvider("CSharp");
WebReferenceOptions __webReferenceOptions = new WebReferenceOptions();
__webReferenceOptions.Style = ServiceDescriptionImportStyle.Client;
// Declares our custom SchemaImporterExtension.
// It maps the wsdl document object type with the types
// associated with the supplied contract interface type.
Type __schemaImporterExtensionGeneric = typeof(SchemaImporterExtension<>);
Type __schemaImporterExtension = __schemaImporterExtensionGeneric.MakeGenericType(_contractType);
__webReferenceOptions.SchemaImporterExtensions.Add(__schemaImporterExtension.AssemblyQualifiedName);
// Build the CodeDOM tree for the proxy client associated with the specified wsdl document.
ServiceDescriptionImporter.GenerateWebReferences(__wbRefs, __csharp, __cplUnit, __webReferenceOptions);
Figure 2 – ServiceDescriptionImporter.GenerateWebReferences.
Implémentation du contrat.
Il reste une dernière étape avant de disposer d'un proxy fortement typé. En effet, le proxy généré suite à l'appel de la méthode GenerateWebReferences n'hérite que de la classe SoapHttpClientProtocol. Il est nécessaire d'adjoindre à cet héritage l'interface (TProxy) définisssant le contrat devant être rempli par le service web distant et donc par le proxy.
// Make inheritance between the generated proxy client
// and the specified contract type associated with the specified wsdl document.
foreach (CodeTypeDeclaration codeTypeDeclaration in __namespace.Types)
{
if (codeTypeDeclaration.BaseTypes.Count > 0)
{
if (codeTypeDeclaration.BaseTypes[0].BaseType == typeof(SoapHttpClientProtocol).FullName)
{
codeTypeDeclaration.BaseTypes.Add(typeof(TProxy));
}
}
}
Figure 3 – Inheritance between the generated proxy client and the specified contract.
Conclusion
En tirant profit des fonctionnalités avancées de la version 2.0 de la plateforme Microsoft .NET, il est possible de bénéficier dés à présent d'une expérience de développement similaire à celle que propose WCF pour communiquer avec des web services.
Examples
// Contracts
public interface IContactManager
{
Contact GetContact(Person person);
}
// Server Side
[WebService(Namespace = "urn:RioterDecker.BluePrints")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class ContactWebService : WebService, IContactManager
{
[WebMethod]
public Contact GetContact(Person person)
{
// Simulate the search of the contact associated with specified person.
Contact __contact = new Contact();
__contact.FirstName = person.FirstName;
__contact.LastName = person.LastName;
__contact.Age = person.Age;
__contact.Phone = "+33 (0) 825 827 829";
__contact.LastModifiedAt = DateTime.Now;
return __contact;
}
}
// Client Side
static void Main(string[] args)
{
ProxyFactory<IContactManager> __proxyFactory = new ProxyFactory<IContactManager>(WSDL);
IContactWebService __proxy = __proxyFactory.CreateProxy();
Person __person = new Person();
__person.FirstName = "Michel";
__person.LastName = "DUGNARD";
__person.Age = 47;
Contact __contact = __proxy.GetContact(__person);
}
Résumé
Vous trouverez, ci-dessous, la liste des concepts abordés par cet article :
Pour aller plus loin
Les exemples de code présentés dans cet article ont été extraits du projet ProxyFactory publié sur le portail communautaire CodePlex.
Ce projet a pour fin de proposer une véritable librairie généralisant ce qui a été présenté dans cet article à différentes technologies de communication tel que Remoting ou WSE.
Téléchargement
Pré-requis NET 2.0
Vous pouvez télécharger à partir du site du projet ProxyFactory : Du code source, un kit de démarrage mais également une vidéo de démonstration.
La version pdf est disponible ici