[C#] Informer des fonctionnalités d'une assembly

Bonjour à tous,

J’explique mon “problème”. Dans l’application sur laquelle je travaille, nous avons une fonctionnalité qui permet aux utilisateurs de créer des modules, sous la forme d’assembly (.dll) .Net permettant de faire des exportation et importation de données.

Il y a une interface graphique dans le programme qui permet à l’utilisateur de chercher sur son disque une assembly et de la charger. A ce moment, nous devons regarder dans la dll qui a été chargée pour vérifier plusieurs choses :

  1. Est-ce que c’est bien une assembly .Net
  2. Est-ce qu’elle contient bien les fonctionnalités qu’on attends d’elle
  3. Quel est le type d’exportation qu’elle est capable de faire
  4. Quel sont les cibles possibles pour ces importations

Les deux premiers points sont faciles à gérer, le premier il suffit de récupérer l’exception qui est levée quand on ouvre l’assembly si elle n’est pas lisible. Pour la seconde j’ai simplement fait une classe de base, abstraite (ExportBase et ImportBase) qui contient la méthode publique DoExport et DoImport qui appelle d’autres méthodes virtual, privées celles-ci qui vont charger les données et les exporter vers la cible (ou l’inverse).

Pour le troisième cas, en gros, j’ai un certains nombre de type d’éléments qui peuvent être importer et exporter, disons des clients, des utilisateurs et des fournisseurs. J’ai une classe pour chacun de ces types.

Lorsque je charge une assembly, elle est susceptible de contenir plusieurs classes du type ExportBase et l’utilisateur doit donc choisir quel type il veut exporter. Je dois donc regarder l’ensemble des classes contenues dans l’assembly et enabler ou disabler des fonctionnalités en fonction des classes présentes dans l’assembly.

Pour faire ça, j’ai créé une interface pour chacun des types. Par exemple, IClientExport. Cette interface hérite de l’interface IExport qui est implémentée par ExportBase. De cette façon, pour vérifier qu’une classe dans l’assembly est capable d’exporter des clients, j’ai juste à vérifier qu’elle implémente bien IClientExport.

Jusque là ça me semble relativement logique, et ça fonctionne bien.

Là où j’ai un doute c’est la manière dont j’ai fait le quatrième cas. Chaque export peut avoir une ou plusieurs cibles, par exemple ma classe qui implémente IClientExport peut exporter mes clients soit vers le disque dur via un fichier, soit vers un web service. Le truc c’est que chaque module peut implémenter une ou plusieurs cibles. Par exemple un module peut n’exporter que vers un web service, un autre que vers des fichiers et un autre encore vers tout. L’utilisateur doit donc pouvoir choisir vers quelle cible il veut effectuer l’exportation, mais j’aimerais restreindre les cibles possibles à celles qui sont disponibles pour le module sélectionné.

La manière dont j’ai implémenté ça est la suivante, dans ma classe ExportBase, j’ai une propriété statique en lecture seule qui me renvoie les cible possibles (une tableau de type énumératif). Dans chacune des classes qui hérites de ExportBase (celles qui implémentent les modules) je redéfinis la propriété statique en utilisant le mot-clef new. C’est un peu cette utilisation du mot-clef new qui me chiffone, mais je ne vois pas bien comment faire ça autrement, c’est un work-around pour la non-existence de l’héritage des attributs statiques de C#… mais je suis certain qu’il y a un autre moyen de faire ça… quelqu’un aurait une idée?

J’ai lu en diagonale, mais pourquoi pas des attributs sur tes méthodes ou ta classe qui définissent les cibles possibles des exports ?

Ton IClientExport (qui donc à priori ne doit présenter aucune méthode vu qu’il sert juste à tagger tes classes) devrait être remplacé par cet attribut [Export(“Client”)] ou [Export(ExportType.Client)] définit sur tes classes qui héritent de ExportBase.

Tu peux aussi créer des attributs qui peuvent être multiples sur une classe, donc par exemple :

[Export(ExportType.Client)]
[Target(TargetType.WebServices)]
[Target(TargetType.Excel)]
public class MyClientExport : ExportBase { … }

Oui, j’ai pensé aux attributs aussi, il me semble que ça reviendrait plus ou moins au même que ce que j’ai implémenté, non? Peut-être en un peu moins bizarre que le new static.

Par contre, est-ce qu’il y a un moyen de faire qu’une classe qui hérite d’une autre classe ou qui implémente une interface soit obligée de définir un attribut?

Ben c’est plus fait pour : tu définis des metadonnées qui décrivent ta classe, sans nuire à l’implémentation en ajoutant des infos qui ne servent pas au fonctionnement à proprement parler.

En ce qui concerne le côté obligatoire, non, tu ne peux rien faire. Mais il me semble assez simple et concevable que ton loader s’occupe de lever des tirs s’il rencontre des classes sans attributs (ou bien qu’il définissent un sens par défaut si aucun attribut n’est présent).

PS: Et les attributs gèrent la notion d’héritage.

Mais dans ton exemple en haut, comment tu fais pour vérifier quand tu charges l’assembly quels target types sont supportés par ta classe?

Si tu as un peu de temps devant toi, ca : http://www.codeplex.com/MEF devrait sortir avec VS 2010 :slight_smile:

Type.GetCustomAttributes() te permet d’énumérer les attributs définis sur ta classe (et les attributs sont instanciés, donc tu récupères la version avec le constructeur déjà appelé.

Dans mon exemple ça donnerait un truc comme ça :

Type reflected; foreach (object attribute un reflected.GetCustomAttributes()) { if (attribute is TargetAttribute) { TargetAttribute targetAttr = (TargetAttribute)attribute; switch (targetAttr.SupportedTarget) { case TargetType.Excel : // blabla break; // .... } }