Chargement des assemblages et des versions

J’envisage d’append une certaine extensibilité à une application existante en fournissant quelques interfaces prédéfinies pouvant être implémentées par des “plugins” déposés à un emplacement spécifique et repris par l’application. Le kernel de l’application est rarement mis à jour alors que les plugins sont mis à jour et déployés plus fréquemment.

Donc, fondamentalement, avoir une configuration comme celle-ci:

// in core assembly (core.dll) public interface IReportProvider{ ssortingng GenerateReport(); } // in other assembly(plugin.dll) public class AwesomeReport : IReportProvider { ssortingng GenerateReport(){ ... } } 

Les deux projets font tous partie du même processus de construction. L’application principale est déployée seule et les plug-ins sont supprimés ultérieurement.

Mon problème est lié à la gestion des versions d’assemblage et à sa résolution dans le temps. Disons que core.dll v1 est déployé et que je souhaite déposer un plugin. Cela fonctionne très bien si plugin.dll fait référence à core.dll v1. Cependant, si le plugin.dll est compilé avec une version ultérieure de core.dll (dans le cadre de la construction, dites v2), le plugin ne se charge pas car il fait référence à core.dll v2 mais la version déployée ne comporte que core.dll. v1.

C’est un comportement raisonnable et attendu, mais cela me donne quelques difficultés à la façon dont ce projet a été mis en place, à savoir que le développement / les mises à jour de plugins ne peuvent pas simplement être effectués en exécutant à nouveau la construction et en déposant les nouveaux plugins (qui ont maintenant été ajoutés). dépendances de version plus récentes).

(Je suis conscient des problèmes potentiels liés à la résolution d’assemblages récents à des assemblages plus anciens et des incohérences potentielles dans les définitions de type. La question concerne uniquement la résolution du problème de résolution d’assembly de haut niveau, et non de la résolution des problèmes liés aux définitions de type incohérentes.)

Je vois quelques options pour que les choses fonctionnent, mais aucune n’est aussi simple que je le voudrais vraiment:

  1. Ajout de liaisons de redirection vers web.config, ordonnant à toutes les références core.dll v1 + de les résoudre en tant que références core.dll v1
  2. Création d’un assemblage ‘contracts.dll’ contenant les définitions d’interface et maintien du numéro de version de cet assemblage spécifique inchangé d’une génération à l’autre
  3. Construire des plugins avec la version déployée de core.dll (référencer en quelque sorte la version déployée en développement)

Comme mentionné, aucune de ces solutions n’est vraiment avantageuse pour moi et j’espère que quelqu’un aura une solution plus intéressante?

J’ai beaucoup travaillé dans ce domaine et j’ai toujours constaté qu’il était nécessaire de définir des limites pour définir exactement les limites de la scope. Il est risqué de vouloir la plus grande extensibilité, mais tout cela a un coût. Ce coût sera soit au moment de la compilation, sous la forme de conventions, de meilleures pratiques et peut-être même de contrôles de construction automatisés, soit dans une exécution compliquée.

Pour adresser les options que vous avez énumérées:

  1. Les redirections de liaisons ne sont qu’un outil partiel pour parvenir à une solution. Ils permettront à votre programme de “glisser” une version d’une DLL à la place d’une autre, mais cela ne résoudra pas comme par magie le problème de ce qui se passe lorsque les méthodes changent. MissingMethodException? Quelque chose ici que vous n’avez peut-être pas pensé d’ailleurs est la chaîne de dépendance. chaînes de dépendance Vous pouvez constater que pendant que l’application traite la dépendance ‘A’ comme un object de la version 1, elle crée en interne quelque chose à partir d’une version ultérieure qui est renvoyée à l’application et convertie en v1.0 – provoquant une exception. Cela peut être délicat à gérer – et n’est que l’un des risques.

  2. Conserver la même version d’assemblage des contrats d’une génération à l’autre Cela peut fonctionner avec élégance. Cependant, il ne fait que reporter la complexité du moment de la construction à l’exécution. Vous devrez faire preuve de diligence pour vous assurer que vos modifications ne briseront pas la compatibilité entre les versions. Sans oublier que, à mesure que votre demande vieillit, vous allez collecter de nombreuses déclarations dans ces contrats que vous voudrez déconseiller. Éventuellement, ce fichier deviendra volumineux, encombrant et source de confusion pour les développeurs – et cela ne représente même pas tous les contrats d’entité que vous avez également!

  3. Je ne suis pas trop sûr de ce que vous entendez par celui-ci et de la manière dont il couvre votre espace de problèmes.

Une autre approche, que nous avons adoptée, consiste à créer un nouveau contrat pour chaque version majeure du ‘SDK’. Cela présente certains avantages politiques dans la mesure où il corrige des fonctionnalités majeures pendant un certain temps, ce qui signifie que nous pouvons conserver les demandes de fonctionnalités à un niveau raisonnable d’attentes. Tout ce qui est au-delà (nécessitant une nouvelle génération de contrat) est différé jusqu’à la prochaine version majeure. ‘. Cela nécessite toutefois de la diligence dans la conception de vos fonctionnalités – rédigez vos contrats avec un peu de prévoyance afin de pouvoir anticiper les exigences les plus évidentes – mais j’estime vraiment que cela va sans dire de toute façon … on les appelle des “contrats”. pour une raison. Chaque nouveau contrat existerait dans un espace de noms de version (Company.Product.Contracts.v1_0, Company.Product.Contracts.v1_1).

Je ne voudrais PAS enchaîner mes contrats (chaque nouvelle version du contrat héritant de la dernière). Cela vous ramène aux problèmes liés au maintien du même numéro de version. Vous ne pouvez jamais vous débarrasser complètement des fonctionnalités sans rompre complètement la chaîne.

Lors du chargement de votre plug-in, il peut demander à l’hôte quel niveau de fonctionnalité il prend en charge (version du contrat) – et s’il s’agit d’un ancien scénario de plug-in hôte / plus récent: programmez le plug-in de manière à réduire ses fonctionnalités d’exécution afin de prendre en charge les capacités moins importantes de l’hôte, ou tout simplement refuser de charger. Quoi qu’il en soit, vous devriez probablement effectuer ces vérifications, car rien ne permet à votre plug-in d’utiliser des fonctionnalités de l’hôte qui n’existent tout simplement pas! La structure MAF de Microsoft tente d’atteindre cet objective en utilisant une infrastructure shim, mais elle entraîne une complexité énorme pour la plupart des utilisateurs.

Voici quelques points à considérer:

  1. Portée de vos exigences d’extensibilité! Tout ce que vous voulez accomplir vous coûtera en entretien courant.
  2. Réfléchissez à la manière dont vous allez éliminer la fonctionnalité
  3. N’oubliez pas que vos contrats contiendront des entités ainsi que des contrats logiques. Ceux-ci ont des considérations légèrement différentes de celles des contrats logiques car ils sont souvent transmis beaucoup plus.
  4. Examinez attentivement si chaque vérification de compatibilité serait mieux effectuée lors de la compilation plutôt que lors de l’exécution (ou inversement)
  5. Numérotation des versions! Les numéros de version d’assemblage sont parfaits pour le comportement à l’exécution, les numéros de version de fichier sont là pour vous aider à suivre une DLL vers la source dans votre contrôle de version – utilisez-les tous les deux!
  6. Si vous effectuez une résolution de DLL personnalisée, utilisez le fichier app.config pour définir vos emplacements de DLL personnalisés, et NON les événements de résolution d’assembly. L’approche de configuration est beaucoup plus prévisible, et, hé, elle est déclarée en XML facilement lisible! Fusion Log Viewer indiquera également avec précision où votre DLL a été insérée dans la chaîne de détection, tandis que l’événement assembly-resolver masquera toute votre logique et vos règles dans le code. Le seul inconvénient est que l’utilisation de app.config signifie que les modifications ne prendront effet que lorsque votre application relira le fichier de configuration (en général, redémarrage de l’application), mais si vous effectuez l’isolation AppDomain de vos plugins, vous pouvez même contourner ce problème.

En ce qui concerne votre problème ‘core.dll’ … Je pense que pour vous, il s’agit d’un problème relativement simple. Tout dans votre DLL de contrats principaux doit exister sous un espace de noms de version (voir ci-dessus, Company.Product.v1_0 etc.), il est donc logique que votre DLL contienne également un numéro de version. Cela supprimera le problème de l’écrasement de vos DLL lors du déploiement dans le dossier bin. NE FAITES PAS CONFIANCE AU GAC – ce sera une douleur à long terme. Même si c’est dommage, les développeurs semblent toujours oublier que le GAC annule tout, et cela peut devenir un cauchemar de débogage – cela influencerait également vos scénarios de déploiement en ce qui concerne les permissions.

Si vous devez conserver le même nom de DLL, vous pouvez créer un “gac local” dans votre application, ce qui vous permettra de stocker vos DLL de manière à ce qu’elles ne se superposent pas, mais puissent toutes être résolues par le runtime. Consultez la «redirection de liaison» dans le fichier app.config ( voir ma réponse ici ). Ceci peut être combiné avec une “structure de dossier pseudo-GAC” dans le dossier bin de votre application. Votre application sera alors capable de localiser n’importe quelle version de la DLL requirejse sans aucune logique de résolution de code personnalisée. Je déploierais toutes les versions précédemment sockets en charge de votre core.dll, avec votre application. Si votre application passe à la version 9 et que vous avez également décidé de prendre en charge les versions 7 et 8 des plug-ins, il vous suffit d’inclure Core.7.dll, Core.8.dll et Core.9.dll. La logique de chargement de votre plug-in doit détecter les dépendances sur les anciennes versions de Core et alerter l’utilisateur que le plug-in n’est pas compatible.

Il y a beaucoup de choses à ce sujet, si je pense à autre chose qui est pertinent pour votre cause, je vérifierai à nouveau …

Quelles que soient vos raisons pour éviter vos 3 points (qui, à mon avis, sont plus fiables et plus aptes), vous pouvez tirer parti de Assembly Resolve Event .

Inclure du code tel que suit dans votre core.dll

 static Assembly AppDomain_AssemblyResolve(object sender, ResolveEventArgs args) { //replace the if check with more reliable check such as matching the public key as well if (args.Name.Contains(Assembly.GetExecutingAssembly().GetName().Name)) { Console.WriteLine("Resolved " + args.Name + " as " + Assembly.GetExecutingAssembly().GetName()); return Assembly.GetExecutingAssembly(); } return null; } 

Liez le gestionnaire ci-dessus avant de tenter de charger vos plugins. Si vous chargez les plugins dans Appdomain actuel, associez-le à AssemblyResolve du domaine actuel.

Par exemple

 [SecurityPermission(SecurityAction.Demand, ControlAppDomain = true)] public static void LoadPlugins() { AppDomain.CurrentDomain.AssemblyResolve += AppDomain_AssemblyResolve; Assembly pluginAssembly = AppDomain.CurrentDomain.Load("MyPlugin"); } 

Vous pourrez peut-être utiliser les redirections de liaison .