Frédéric Colin
WCF : L’extensibilité par la pratique – L’exemple
Dans le précédent volet, je vous ai expliqué une partie des fondamentaux de l’extensibilité WCF. Je vous ai aussi décrit le fonctionnel d’un exemple plus complet que je vais maintenant concevoir et développer pour illustrer l’invocation d’opération.
Par Frédéric Colin publié le 13/09/2009 à 22:18, lu 1795 fois, 9 pages
 6 | Implémentation du contrat et du service façade
Je ne m’étendrai pas énormément sur cette partie là tellement il existe d’exemples sur le Web (Google et Bing sont vos amis). Toutefois, je me permets de référencer un excellent article sur le sujet de M. Russell Jones, que je vous incite fortement à lire si vous débutez sur CodeDom. Même s’il date un peu et même si certaines manières de faire sont obsolètes, c’est une très bonne base de travail. A noter que l’article est en VB, mais il est très facile de traduire en C# les exemples de code. Je me permettrai juste de reprendre et de remettre au goût du jour le tableau de l’article en expliquant les différentes étapes pour générer du code :
  • Etape 1 : création d’un espace de nom
  • Etape 2 : import des espaces de nom utiles
  • Etape 3 : création d’une classe (ou d’une interface) et ajout de cette dernière dans l’espace de nom produit précédemment.
  • Etape 4 : création des champs dans la classe précédente avec un nom et un type.
  • Etape 5 : création des méthodes et des propriétés
  • Etape 6 : remplir le corps des méthodes et des propriétés précédemment créées par l’ajout du code correspondant
  • Etape 7 : ajout des méthodes et des propriétés à la classe précédemment créée.
  • Etape 8 : création du fournisseur de code adéquat (C#, VB, etc.).
  • Etape 9 : utilisation de la méthode « GenerateCodeFromNamespace » sur le provider précédent afin de générer le code correspondant.
  • Etape 10 : utilisation de la méthode « CompileAssemblyFromSource » sur le provider précédent afin de générer la nouvelle Assembly en mémoire ou physiquement.
  • Etape 11 : vérification des éventuelles erreurs de compilation issues de l’étape précédente.
Voici le code de la méthode principale de la classe DynamicHost, appelée par le programme principale du processus porteur :

public Assembly GetContractAssembly()

{

    // Namespace creation

    var ns = new CodeNamespace(ConfigurationManager.AppSettings["NamespaceToGenerate"]);

 

    // Add useful "using"

    ns.Imports.AddRange(

        (from String s in ConfigurationManager.AppSettings["UsefulUsings"].Split(";".ToCharArray())

        select new CodeNamespaceImport(s)).ToArray()

        );

 

    // Create the facade Service Contract

    var inter = new System.CodeDom.CodeTypeDeclaration(ConfigurationManager.AppSettings["FacadeName"]);

    inter.IsInterface = true;

    inter.CustomAttributes.Add(new CodeAttributeDeclaration("System.ServiceModel.ServiceContract"));

 

    // Add the facade service contract to the namespace

    ns.Types.Add(inter);

Vous noterez les points importants suivants :
  • Finalement, CodeDom est un jeu de mécano où chaque type d’instruction possède une définition abstraite (dans le sens agnostique d’un point de vue du langage).
  • L’ajout d’un bloc (méthode « AddRange ») des « using » qui vont bien via l’utilisation d’une requête Linq à partir du fichier de configuration (en gros un appSettings avec des valeurs séparées par des « ; »). Encore une fois, l’utilisation du split pourrait probablement être avantageusement remplacée par une méthode plus élégante (expression régulière). J’ai en fait cherché la concision au détriment du reste.
  • L’interface créée représente le point central sur lequel on va ajouter du code, par exemple une interface. D’ailleurs, vous noterez que d’un point de vue CodeDom, une interface est une déclaration de type sur lequel on précise qu’il s’agit d’une interface.
  • Cette interface est ensuite ajoutée à la collection des types de l’espace de nom.

// Build Fake Facade Service

var cls = new System.CodeDom.CodeTypeDeclaration(ConfigurationManager.AppSettings["ServiceName"]);

cls.BaseTypes.Add(inter.Name);

cls.IsClass = true;

 

ns.Types.Add(cls);

Vous noterez les points importants suivants :
  • L’ajout d’une implémentation d’interface via la collection des types de base (« BaseTypes ») de ladite classe.

// Create Operation Contracts on the facade

foreach (String f in Directory.GetFiles(ConfigurationManager.AppSettings["DirectoryToMonitor"]))

{

    CreateOperationContractAndImplementation(f, inter, cls);

}

Vous noterez les points importants suivants :
  • La création de la façade se fait en fonction des Assemblies qui seront stockées dans un répertoire donné. Effectivement, j’aurai pu vérifier s’il s’agissait bel et bien d’Assemblies ;-).
  • Le gros du travail est réalisé dans la méthode privée « CreateOperationContractAndImplementation(…) ».
Je ne vais pas recopier complètement le code de cette méthode dans l’article, mais je vais juste mettre en avant quelques points qui me paraissent importants et que vous pourrez consulter dans le code joint à l’article :
  • Pour une Assembly donnée chargée en mémoire, l’objectif est de parcourir par « Reflection » l’ensemble des types contenus.
  • Ensuite, pour chaque type, je récupère l’ensemble des méthodes statiques et d’instances publiques.
  • S’en suit une petite gestion pour l’éventuel renommage des méthodes surchargées. Puis je crée la méthode : (l’exemple qui suit est considéré sans surcharge) :

method.Name = m.Name;

method.CustomAttributes.Add(

  new CodeAttributeDeclaration(

    "System.ServiceModel.OperationContract",

    new CodeAttributeArgument(

        new CodeSnippetExpression(

            String.Format("Action=\"{0}/{1}/{2}/{3}\"",

                t.Assembly.GetName().Name, t.FullName, m.Name, method.Name)

        )

    )

  )

);

methodForService.Name = m.Name;

Au passage, vous noterez l’ajout de l’attribut « OperationContract » avec une valeur de propriété « Action » construite de manière à gérer le dispatch correctement en sauvegardant le nom de l’Assembly d’origine, le nom de la classe, le nom de la méthode et le nom de la méthode redéfinie si besoin.
  • Pour la création de la méthode implémentée, il faut penser que par défaut elle sera créée virtuelle sauf si l’on positionne les attributs suivants en CodeDom :

methodForService.Attributes = MemberAttributes.Public | MemberAttributes.Final;

Au niveau de l’implémentation des méthodes de la façade, il a fallu gérer les méthodes de type fonction qui renvoyaient donc des valeurs. Pour cela, j’ai choisi de faire retourner les valeurs par défaut des types valués et « null » pour les types référencés. Ce qui nous donne :

private static void GenerateReturnValueIfNeeded(

    MethodInfo m, CodeMemberMethod method, CodeMemberMethod methodForService)

{

    if ("void" == m.ReturnType.Name.ToLower())

    {

        method.ReturnType = null;

        methodForService.ReturnType = null;

    }

    else

    {

        if (!m.ReturnType.IsGenericType)

        {

            method.ReturnType = new CodeTypeReference(m.ReturnType.FullName);

            methodForService.ReturnType = new CodeTypeReference(m.ReturnType.FullName);

        }

        else

        {

            CodeTypeReference[] typeArguments

                = (from Type arg in m.ReturnType.GetGenericArguments()

                   select new CodeTypeReference(arg.FullName)).ToArray();

 

            CodeTypeReference genericType = new CodeTypeReference(

                m.ReturnType.GetGenericTypeDefinition().FullName.Split('`')[0], typeArguments);

 

            method.ReturnType = genericType;

            methodForService.ReturnType = genericType;

 

        }

 

        // Generate return

        if (m.ReturnType.IsValueType)

            methodForService.Statements.Add(new CodeMethodReturnStatement(

                new CodeArgumentReferenceExpression(String.Format("default({0})", m.ReturnType.Name))));

        else

            methodForService.Statements.Add(new CodeMethodReturnStatement(new CodePrimitiveExpression(null)));

 

    }

}

Vous noterez les points importants suivants sur la génération des valeurs retournées :
  • La gestion des types génériques a nécessité un peu plus de travail. En effet, par réflexion nous récupérons par exemple, ce type de syntaxe pour une liste générique : « System.Collections.Generic.List`1 ».
Il s’avère que cette syntaxe, si on la prend telle quelle, ne compilera pas. D’où le code spécifique de traitement précédent.
Toujours au niveau de l’implémentation des méthodes de la façade, il a fallu gérer les paramètres passés à la méthode. C’est l’objet de la méthode suivante :

private static void GenerateParameters(MethodInfo m, CodeMemberMethod method,

    CodeMemberMethod methodForService)

{

    foreach (ParameterInfo p in m.GetParameters())

    {

        CodeParameterDeclarationExpression exp = new CodeParameterDeclarationExpression(

            p.ParameterType, p.Name);

 

        method.Parameters.Add(exp);

        methodForService.Parameters.Add(exp);

    }

}

Vous noterez les points importants suivants sur la génération des paramètres :
  • On commence par parcourir par réflexion la collection des paramètres de la méthode.
  • Vous noterez que j’utilise deux méthodes distinctes en CodeDom (« CodeMemberMethod ») : l’une pour l’interface, l’autre pour l’implémentation de la méthode.

// Code management

CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");

StringBuilder CSharpCode = new StringBuilder();

 

CodeGeneratorOptions options = new CodeGeneratorOptions();

options.IndentString = "\t";

 

provider.GenerateCodeFromNamespace(ns, new StringWriter(CSharpCode), options);

 

// Compiler options

CompilerParameters parameters = new CompilerParameters();

parameters.GenerateExecutable = false;

parameters.IncludeDebugInformation = false;

parameters.GenerateInMemory = true;

 

// Add useful References

parameters.ReferencedAssemblies.AddRange(

    ConfigurationManager.AppSettings["AssembliesToReference"].Split(";".ToCharArray()));

 

// Tip to add specific reference to WCF since simple name (System.ServiceModel.dll) is not sufficient

parameters.ReferencedAssemblies.Add(typeof(ServiceHost).Assembly.Location);

 

// Parametrize generated Assembly

parameters.OutputAssembly = String.Format("{0}\\{1}.dll",

    Path.GetDirectoryName(Assembly.GetEntryAssembly().Location),

    ConfigurationManager.AppSettings["NamespaceToGenerate"]);

 

// Launch compilation

CompilerResults results = provider.CompileAssemblyFromSource(parameters, CSharpCode.ToString());

Vous noterez les points importants suivants :
  • La classe « CodeDomProvider » possède une méthode statique (« CreateProvider ») pour créer une instance d’un provider en fonction du nom du langage cible.
  • Utilisation d’un « StringBuilder » et d’un « StringWriter » afin de générer le source en C#.
  • Concernant la référence vers l’Assembly « System.ServiceModel.dll », il a fallu ajouter le chemin d’accès spécifique pour que la compilation fonctionne correctement. Pour éviter de coder en dur ce chemin, j’ai préféré le récupérer par introspection via le code suivant :

parameters.ReferencedAssemblies.Add(typeof(ServiceHost).Assembly.Location);

  • Le paramétrage du nom de l’Assembly en mémoire est réalisé via un paramètre stocké dans le fichier de configuration
  • La compilation en mémoire (fonction des paramètres passés) est réalisé à l’aide de la méthode « CompileAssemblyFromSource » du provider et renvoie une instance indiquant le résultat de la compilation ainsi que l’Assembly chargée en mémoire. C’est à partir de cette instance que l’on peut vérifier les éventuelles erreurs de compilation.

    if (results.Errors.Count != 0)

    {

        System.Console.WriteLine("Compilation errors!");

 

        foreach (CompilerError error in results.Errors)

            System.Console.WriteLine(error.ErrorText);

 

        throw new Exception("Errors during compilation!");

    }

 

    provider.Dispose();

 

    return results.CompiledAssembly;

}

Vous noterez les points importants suivants :
  • L’instance de la classe « CompilerResults » possède deux propriétés importantes : une collection des erreurs de compilation (« Errors ») ainsi que l’Assembly générée en mémoire (« Compiled Assembly »).
  • Ne pas oublier de libérer au plus tôt le Provider.
Au final, voici d’ailleurs le code C# généré par CodeDom pour cet article :

namespace THB.Sample.ServiceContracts

{

    using System;

    using System.Reflection;

    using System.ServiceModel;

    using THB.Sample.ServiceModel.Extensions;

 

    [System.ServiceModel.ServiceContract()]

    public interface IFacade

    {

        [System.ServiceModel.OperationContract(

            Action = "THB.Sample.Services/THB.Sample.Services.Service1/DoNothing/DoNothing")]

        [THB.Sample.ServiceModel.Extensions.FacadeOperationBehavior()]

        void DoNothing();

 

        [System.ServiceModel.OperationContract(Action = "THB.Sample.Services/THB.Sample.Services.Service1/Add/Add")]

        [THB.Sample.ServiceModel.Extensions.FacadeOperationBehavior()]

        int Add(int i, int j);

 

        [System.ServiceModel.OperationContract(Name = "Add1",

            Action = "THB.Sample.Services/THB.Sample.Services.Service1/Add/Add1")]

        [THB.Sample.ServiceModel.Extensions.FacadeOperationBehavior()]

        int Add(int i, int j, int k);

 

        [System.ServiceModel.OperationContract(Name = "Add2",

            Action = "THB.Sample.Services/THB.Sample.Services.Service1/Add/Add2")]

        [THB.Sample.ServiceModel.Extensions.FacadeOperationBehavior()]

        int Add(int i, int j, int k, int l);

 

        [System.ServiceModel.OperationContract(

            Action = "THB.Sample.Services/THB.Sample.Services.Service1/AddWithConstant/AddWithConstant")]

        [THB.Sample.ServiceModel.Extensions.FacadeOperationBehavior()]

        int AddWithConstant(int i);

 

        [System.ServiceModel.OperationContract(

            Action = "THB.Sample.Services/THB.Sample.Services.Service1/Minus/Minus")]

        [THB.Sample.ServiceModel.Extensions.FacadeOperationBehavior()]

        int Minus(int i, int j);

 

        [System.ServiceModel.OperationContract(Action = "THB.Sample.Services/THB.Sample.Services.Service1/Mult/Mult")]

        [THB.Sample.ServiceModel.Extensions.FacadeOperationBehavior()]

        int Mult(int i, int j);

 

        [System.ServiceModel.OperationContract(Action = "THB.Sample.Services/THB.Sample.Services.Service1/Div/Div")]

        [THB.Sample.ServiceModel.Extensions.FacadeOperationBehavior()]

        int Div(int i, int j);

 

        [System.ServiceModel.OperationContract(

            Action = "THB.Sample.Services2/THB.Sample.Services2.CustomerService/GetAll/GetAll")]

        [THB.Sample.ServiceModel.Extensions.FacadeOperationBehavior()]

        System.Collections.Generic.List<THB.Sample.DataContracts.Customer> GetAll();

 

        [System.ServiceModel.OperationContract(

            Action = "THB.Sample.Services2/THB.Sample.Services2.CustomerService/GetOrders/GetOrders")]

        [THB.Sample.ServiceModel.Extensions.FacadeOperationBehavior()]

        System.Collections.Generic.List<THB.Sample.DataContracts.Order> GetOrders(int customerID);

 

        [System.ServiceModel.OperationContract(Name = "Add3",

            Action = "THB.Sample.Services2/THB.Sample.Services2.CustomerService/Add/Add3")]

        [THB.Sample.ServiceModel.Extensions.FacadeOperationBehavior()]

        void Add(THB.Sample.DataContracts.Customer c);

 

        [System.ServiceModel.OperationContract(Name = "Add4",

            Action = "THB.Sample.Services2/THB.Sample.Services2.CustomerService/Add/Add4")]

        [THB.Sample.ServiceModel.Extensions.FacadeOperationBehavior()]

        void Add(THB.Sample.DataContracts.Customer[] c);

 

        [System.ServiceModel.OperationContract(

            Action = "THB.Sample.Services2/THB.Sample.Services2.CustomerService/GetArray/GetArray")]

        [THB.Sample.ServiceModel.Extensions.FacadeOperationBehavior()]

        THB.Sample.DataContracts.Customer[] GetArray();

    }

 

    public class Facade : IFacade

    {

        public void DoNothing()

        {

        }

 

        public int Add(int i, int j)

        {

            return default(Int32);

        }

 

        public int Add(int i, int j, int k)

        {

            return default(Int32);

        }

 

        public int Add(int i, int j, int k, int l)

        {

            return default(Int32);

        }

 

        public int AddWithConstant(int i)

        {

            return default(Int32);

        }

 

        public int Minus(int i, int j)

        {

            return default(Int32);

        }

 

        public int Mult(int i, int j)

        {

            return default(Int32);

        }

 

        public int Div(int i, int j)

        {

            return default(Int32);

        }

 

        public System.Collections.Generic.List<THB.Sample.DataContracts.Customer> GetAll()

        {

            return null;

        }

 

        public System.Collections.Generic.List<THB.Sample.DataContracts.Order> GetOrders(int customerID)

        {

            return null;

        }

 

        public void Add(THB.Sample.DataContracts.Customer c)

        {

        }

 

        public void Add(THB.Sample.DataContracts.Customer[] c)

        {

        }

 

        public THB.Sample.DataContracts.Customer[] GetArray()

        {

            return null;

        }

    }

}

Vous noterez une petite incohérence (sans conséquence importante) dans le code généré. En effet, j’ai utilisé le même espace de nom à la fois pour le contrat de service de la façade que pour son implémentation « fake ». C’est mal !
 
» Démarrer une discussion