I Contexte
Nous avons du code legacy avec une application C++ qui communique directement avec une base de données. L’objectif est de migrer progressivement le parc logiciel vers une architecture orientée services (SOA). Pour se faire plusieurs options d’offrent à nous.
Poc.Compatibility Sources (Nécessite le framework .net 4.5 et Visual Studio 2013)
II Solutions possibles
- Générer un proxy en C à partir du fichier .wsdl qui contient la définition de notre webservice
- En utilisant sproxy.exe
- En utilisant la commande « WsUtil.exe *.wsdl *.xsd »
- En utilisant GSOAP (http://gsoap2.sourceforge.net/ )
- Exposer notre service WCF en REST
- En utilisant C++ REST DSK (http://casablanca.codeplex.com/ )
- Ecrire un wrapper en C# qui appelle le webservice et expose les résultats à travers des objets COM
Cet article présente comment mettre en œuvre la dernière solution.
III Réalisation
Le schéma ci-dessous représente la solution proposée. Pour nos besoins de tests nous créons une application console qui va héberger le service WCF. La librairie WCF_COMObject (ici représentée sous le nom de « COM Component ») se chargera d’effectuer les appels au webservice et exposera à son tour les objets pour les rendre accessible depuis C++.
Nous traduisons notre schéma conceptuel en solution sous Visual Studio 2013.
Voici un récapitulatif des projets dans la solution ainsi que leurs rôles respectifs.
Nom du projet | Description |
Poc.Compatibility.ServiceWCF | Implémente le « POCService » |
Poc.Compatibility.Shared.Contracts | Contient les définitions d’interfaces (IPOCService, objets DTO partagés par le serveur et le client) |
Poc.Compatibility.ServiceLauncher | Héberge le service WCF |
Poc.Compatibility.Client | Simple testeur en C# pour vérifier que le service WCF est disponible et qu’il renvoie les résultats attendus. |
Poc.Compatibility.WCF_COMObject | Appelle le webservice, log les appels côté client, expose des objets COM. |
Poc.Compatibility.Win32App | Client C++ natif qui appelle notre objet COM |
Important : Il est préférable d’exécuter Visual Studio 2013 en mode administrateur pour 2 raisons. La première pour pouvoir exécuter le service WCF sans changer la configuration Windows, la seconde pour enregistrer le composant COM automatiquement sans exécuter Resgsm.exe.
Etape 1 : Création d’un service WCF (POCService)
Nous définissions le contrat d’interface suivant :
[ServiceContract] public interface IPOCService { [OperationContract] string GetData(int value); [OperationContract] PersonDTO GetPerson(); } |
La méthode GetPerson() nous intéresse particulièrement car elle renvoie un type complexe « PersonDTO » définit tel que :
[DataContract] public class PersonDTO { [DataMember] public System.Guid Id { get; set; } [DataMember] public string Name { get; set; } } |
L’implémentation de la méthode GetPerson() dans le service consiste à générer un GUID et à remplir la propriété « Name » aléatoirement en se basant sur une base de prénoms existants.
Etape 2 : Hoster le service WCF dans une application console
Cette étape n’est pas indispensable mais pratique pour débugguer facilement notre service WCF. L’utilisation de la classe ServiceHost (namespace System.ServiceModel) permet d’héberger le service dans une application console.
static void Main(string[] args) { try { Uri baseAddress = new Uri("http://localhost:64954/POCService.svc"); using (ServiceHost host = new ServiceHost(typeof(POCService))) { BasicHttpBinding binding1 = new BasicHttpBinding(); binding1.CloseTimeout = TimeSpan.FromMinutes(10); binding1.OpenTimeout = TimeSpan.FromMinutes(10); binding1.ReceiveTimeout = TimeSpan.FromMinutes(10); binding1.SendTimeout = TimeSpan.FromMinutes(10); binding1.MaxBufferPoolSize = 2147483647; binding1.MaxReceivedMessageSize = 2147483647; XmlDictionaryReaderQuotas myReaderQuotas = new XmlDictionaryReaderQuotas(); myReaderQuotas.MaxStringContentLength = 2147483647; myReaderQuotas.MaxArrayLength = 2147483647; myReaderQuotas.MaxBytesPerRead = 2147483647; myReaderQuotas.MaxDepth = 2147483647; myReaderQuotas.MaxNameTableCharCount = 2147483647; binding1.GetType().GetProperty("ReaderQuotas").SetValue(binding1, myReaderQuotas, null); host.AddServiceEndpoint(typeof(IPOCService), binding1, baseAddress); host.Open(); Console.WriteLine("The service is ready at {0}", baseAddress); Console.ReadLine(); // Close the ServiceHost host.Close(); } } catch (Exception ex) { Console.WriteLine("ERROR IN SERVICE. " + ex.Message); Console.ReadLine(); } } |
Etape 3 : Création de la librairie C# qui va consommer le service et exposer ses méthodes en COM
Il s’agit de la partie qui nous intéresse le plus c’est-à-dire la réalisation du Wrapper pour faire communiquer WCF et C++. Nous allons devoir répondre à plusieurs problématiques :
- Notre application C++ étant en Win32, nous devons tout d’abord définir le Build du projet pour cibler une architecture x86.
- Comment enregistrer notre dll en tant que composant COM ? Pour se faire allez dans les « propriétés du projet > Build > Output » il est également nécessaire de cocher l’option « Register for COM interop ».
- Un composant COM n’est pas censé avoir de fichier de configuration associé il faudra donc embarquer ces éléments dans le code (ou laisser le client C++ le faire si on le souhaite).
- Ajouter des logs dans le wrapper
- Rendre visible les objets pour l’application C++
- Signer l’application à l’aide du couple clé publique/clé privée
Organisation du projet :
Tout d’abord nous ajoutons une web reference vers le POCService.
Nous créons ensuite le point d’entrée de notre wrapper : la classe WCF_COM_Class. Notre client C++ va appeler les méthodes de cette classe qui à son tour va requêter le service WCF.
Pour rendre la classe WCF_Com_Class visible il est tout d’abord nécessaire de définir l’interface qu’elle implémentera.
- Création de l’interface WCF_COM_Interface
/// <summary> /// Expose methods will be used by C++ client /// </summary> [ComVisible(true)] [InterfaceType(ComInterfaceType.InterfaceIsDual)] [Guid("12C124D9-204A-408C-B920-CF1561831D5D")] public interface WCF_COM_Interface { [DispId(1)] void Initialize(); [DispId(2)] Person GetPerson(); } |
Attributs de classe :
Nous précisions tout d’abord que l’interface doit-être exposée en COM à travers la propriété « ComVisible ».
La propriété InterfaceType identifie comment exposer une interface à COM, par défaut nous conservons la valeur « InterfaceIsDual » qui autorise les liaisons anticipées et tardives (la plus flexible).
Nous générons un Guid depuis Visual Studio à l’aide du menu « TOOLS > Create Guid», le pattern 5 correspond à nos besoins.
Méthodes :
Les méthodes exposées doivent être tagguées de l’attribut (DispId) dans le cas ou le type d’interface retenue est « InterfaceIsDispatch » il permet de forcer m’attribution d’un id à une méthode.
La méthode Initialize nous permettra de définir la configuration de log4net et d’automapper (nous y reviendront plus tard)
Il est à noter que la méthode GetPerson() renvoie un objet de type Person et non pas de type PersonDTO qui a transité sur le réseau.
- Création de l’implémentation WCF_COM_Class
Comme pour l’interface, nous définissons la propriétée « ComVisible » à « true » et attribuons un Guid.
En définissant l’attribut ClasseInterface à ClassInterfaceType.none, nous devons définir de manière explicite l’interface COM qui sera utilisé par le client C++. Il est également possible de générer automatiquement cette interface bien que cela introduise un risque d’interruption du client en cas de modification.
L’attribut ComSourceInterfaces permet quant à lui de gérer des levées d’évènements en Com (bien que nous n’avons pas ce besoin dans notre exemple).
/// <summary> /// Implementation of exposed methods will be used by C++ client /// </summary> [ComVisible(true)] [Guid("FDCA72A7-5FAE-4BDB-AFD4-853ECD6E31EA"), ClassInterface(ClassInterfaceType.None), ComSourceInterfaces(typeof(WCF_COM_Events))] public class WCF_COM_Class : WCF_COM_Interface { private static readonly ILog log = LogManager.GetLogger(typeof(WCF_COM_Class).Name); public void Initialize() { Mapper.CreateMap<PersonDTO, Person>(); LoggerTools.Setup(); } public Person GetPerson() { log.Debug("Calling GetPerson()"); try { POCServiceClient client = WCFTools.GetClient(); var personDTO = client.GetPerson(); client.Close(); log.DebugFormat("Person created: Id={0} Name={1}", personDTO.Id, personDTO.Name); return Mapper.Map<PersonDTO, Person>(personDTO); } catch(Exception ex) { log.Debug("Calling GetPerson() failed", ex); throw ex; } } } |
Méthodes :
Comme dit précédemment au niveau de l’interface, une méthode Initialize() devra être appelée avant tout autre appel depuis le client C++. Nous initialisons tout d’abord la configuration d’AutoMapper (qui va nous permettre de convertir un objet « PersonDTO » en objet de type « Person » sans effort) puis nous configurons log4net en code. (voir le code source pour plus de détails).
La méthode GetPerson() vas exécuter l’appel au webservice puis transformer notre objet de type PersonDTO (non exposé en tant que COM) en objet Person.
- Définition de l’objet Person
En se basant sur le même principe que pour WCF_COM_Interface nous définissons l’interface IPerson tel que :
/// <summary> /// Expose "Person" object to be visible from C++ client /// </summary> [ComVisible(true)] [InterfaceType(ComInterfaceType.InterfaceIsDual)] [Guid("AAE6125D-C954-4A15-81D3-7E9034B43FD5")] public interface IPerson { [DispId(1)] Guid Id { get; set; } [DispId(2)] string Name { get; set; } } |
Note : Nous définissons les propriétés dans l’interface comme des méthodes. Les getters/setters seront générés pour le client C++.
L’interface est implémentée telle que :
[ComVisible(true)] [Guid("709DBD54-F2E7-4266-8F8C-6F0E839B13BD"), ClassInterface(ClassInterfaceType.None)] public class Person : IPerson { public Guid Id { get; set; } public string Name { get; set; } } |
- Configuration du projet
Dans les propriétés du projet, nous définissons le build pour cibler une architecture x86.
De plus, il est nécessaire d’activer l’option « Register for COM interop ».
- Signature de la librairie
Etape indispensable pour publier notre composant COM, la signature passe par un fichier .snk que nous allons générer à l’aide de l’outil Sn.exe. (Plus d’informations sur http://msdn.microsoft.com/en-us/library/6f05ezxy(v=vs.110).aspx )
Une fois le fichier WCF_COM_Key.snk généré, on ajoute une référence dans le fichier AssemblyInfo.cs
[assembly: AssemblyKeyFile("WCF_COM_Key.snk")] |
- Compilation du projet
La compilation du projet va également enregistrer le composant COM et générer un fichier .tln qui contient les définitions des objets exposés. Ce fichier « Poc.Compatibility.WCF_COMObject.tlb » devra être importé dans l’application C++.
Etape 4 : Création d’une application Win32
Nous créons un projet console en C++ de type Win32. Par défaut j’ai désactivé la précompilation des headers. Le fichier Poc.Compatibility.Win32App.cpp contient le main de l’application.
// Poc.Compatibility.Win32App.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include #import "..\Debug\Poc.Compatibility.WCF_COMObject.tlb" named_guids raw_interfaces_only int _tmain(int argc, _TCHAR* argv[]) { HRESULT hRes = S_OK; CoInitialize(NULL); //Instanciate our COM interface Poc_Compatibility_WCF_COMObject::WCF_COM_InterfacePtr p(__uuidof(Poc_Compatibility_WCF_COMObject::WCF_COM_Class)); //Intialize before calling others methods p->Initialize(); //Calling GetPerson method and display the results Poc_Compatibility_WCF_COMObject::IPerson *person; try { p->GetPerson(&person); if (person == NULL) { throw "Person null"; } BSTR name; person->get_Name(&name); GUID id; BSTR bstrId; person->get_Id(&id); StringFromCLSID(id, &bstrId); std::wcout << "Name=" << name << " Id=" << bstrId; } catch (...) { std::cout << "Error when calling GetPerson()"; } //Wait to keep alive the console std::cout << "\n"; system("PAUSE"); return 0; } |
Nous pouvons tout d’abord voir l’ajout de la référence vers le fichier Poc.Compatibility.WCF_COMObject.tlb généré à partir de notre wrapper C#. On accède ainsi au namespace « Poc_Compatibility_WCF_COMObject ».
On instancie la classe « WCF_COM_Class » puis on appelle la méthode Initialize() qui a, comme vu précemmdent, pour rôle de configurer log4net et AutoMapper.
Pour appeler la méthode GetPerson() , nous passons une référence vers un objet de type « Person » déclaré au préalable.
IV Conclusion
Il existe plusieurs façons d’accéder à un service WCF depuis C++. Pour faciliter l’intégration des mises à jour du contrat et faciliter le debug du service développé le client en C# permet d’isoler facilement la partie communication. La contre-partie est qu’il faille reprendre les méthodes du webervice pour les ré-exposer côté COM ce qui dans l’absolu pourrait être optimisé pour éviter la redondance.
Comments