web-dev-qa-db-fra.com

Manière correcte de charger l'assembly, les méthodes Find Class et Call Run ()

Exemple de programme de console.

class Program
{
    static void Main(string[] args)
    {
        // ... code to build dll ... not written yet ...
        Assembly assembly = Assembly.LoadFile(@"C:\dyn.dll");
        // don't know what or how to cast here
        // looking for a better way to do next 3 lines
        IRunnable r = Assembly.CreateInstance("TestRunner");
        if (r == null) throw new Exception("broke");
        r.Run();

    }
}

Je veux construire dynamiquement un Assembly (.dll), puis le charger, instancier une classe et appeler la méthode Run () de cette classe. Devrais-je essayer d'incarner la classe TestRunner? Je ne sais pas comment les types d'un assemblage (code dynamique) sauraient à propos de mes types dans mon (application statique Assembly/Shell). Est-il préférable d'utiliser quelques lignes de code de réflexion pour appeler Run () sur un seul objet? À quoi ce code devrait-il ressembler?

MISE À JOUR: William Edmondson - voir le commentaire

78
BuddyJoe

Utiliser un domaine d'application

Il est plus sûr et plus flexible de charger l’Assemblée dans son propre AppDomain d’abord.

Donc au lieu de la réponse donnée précédemment :

var asm = Assembly.LoadFile(@"C:\myDll.dll");
var type = asm.GetType("TestRunner");
var runnable = Activator.CreateInstance(type) as IRunnable;
if (runnable == null) throw new Exception("broke");
runnable.Run();

Je suggérerais ce qui suit (adapté de cette réponse à une question connexe ):

var domain = AppDomain.CreateDomain("NewDomainName");
var t = typeof(TypeIWantToLoad);
var runnable = domain.CreateInstanceFromAndUnwrap(@"C:\myDll.dll", t.Name) as IRunnable;
if (runnable == null) throw new Exception("broke");
runnable.Run();

Vous pouvez maintenant décharger l'assembly et définir différents paramètres de sécurité.

Si vous voulez encore plus de flexibilité et de puissance pour le chargement et le déchargement dynamiques des assemblages, vous devez vous reporter à la structure de compléments gérés (c'est-à-dire le System.AddIn espace de noms). Pour plus d'informations, consultez cet article sur Compléments et extensibilité sur MSDN .

70
cdiggins

Si vous n'avez pas accès aux informations de type TestRunner dans l'assembly appelant (vous semblez ne pas pouvoir le faire), vous pouvez appeler la méthode comme suit:

Assembly assembly = Assembly.LoadFile(@"C:\dyn.dll");
Type     type     = Assembly.GetType("TestRunner");
var      obj      = Activator.CreateInstance(type);

// Alternately you could get the MethodInfo for the TestRunner.Run method
type.InvokeMember("Run", 
                  BindingFlags.Default | BindingFlags.InvokeMethod, 
                  null,
                  obj,
                  null);

Si vous avez accès au type d'interface IRunnable, vous pouvez y convertir votre instance (plutôt que le type TestRunner qui est implémenté dans l'assembly créé ou chargé dynamiquement, n'est-ce pas?):

  Assembly assembly  = Assembly.LoadFile(@"C:\dyn.dll");
  Type     type      = Assembly.GetType("TestRunner");
  IRunnable runnable = Activator.CreateInstance(type) as IRunnable;
  if (runnable == null) throw new Exception("broke");
  runnable.Run();
46
Jeff Sternal

Je fais exactement ce que vous recherchez dans mon moteur de règles, qui utilise CS-Script pour la compilation, le chargement et l'exécution dynamiques de C #. Cela devrait être facilement traduisible dans ce que vous recherchez, et je vais vous donner un exemple. Tout d'abord, le code (dépouillé):

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using CSScriptLibrary;

namespace RulesEngine
{
    /// <summary>
    /// Make sure <typeparamref name="T"/> is an interface, not just any type of class.
    /// 
    /// Should be enforced by the compiler, but just in case it's not, here's your warning.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class RulesEngine<T> where T : class
    {
        public RulesEngine(string rulesScriptFileName, string classToInstantiate)
            : this()
        {
            if (rulesScriptFileName == null) throw new ArgumentNullException("rulesScriptFileName");
            if (classToInstantiate == null) throw new ArgumentNullException("classToInstantiate");

            if (!File.Exists(rulesScriptFileName))
            {
                throw new FileNotFoundException("Unable to find rules script", rulesScriptFileName);
            }

            RulesScriptFileName = rulesScriptFileName;
            ClassToInstantiate = classToInstantiate;

            LoadRules();
        }

        public T @Interface;

        public string RulesScriptFileName { get; private set; }
        public string ClassToInstantiate { get; private set; }
        public DateTime RulesLastModified { get; private set; }

        private RulesEngine()
        {
            @Interface = null;
        }

        private void LoadRules()
        {
            if (!File.Exists(RulesScriptFileName))
            {
                throw new FileNotFoundException("Unable to find rules script", RulesScriptFileName);
            }

            FileInfo file = new FileInfo(RulesScriptFileName);

            DateTime lastModified = file.LastWriteTime;

            if (lastModified == RulesLastModified)
            {
                // No need to load the same rules twice.
                return;
            }

            string rulesScript = File.ReadAllText(RulesScriptFileName);

            Assembly compiledAssembly = CSScript.LoadCode(rulesScript, null, true);

            @Interface = compiledAssembly.CreateInstance(ClassToInstantiate).AlignToInterface<T>();

            RulesLastModified = lastModified;
        }
    }
}

Cela prendra une interface de type T, compilera un fichier .cs dans un Assembly, instanciera une classe d'un type donné et alignera cette classe instanciée sur l'interface T. En gros, vous devez simplement vous assurer que la classe instanciée implémente cette interface. J'utilise des propriétés pour configurer et accéder à tout, comme ceci:

private RulesEngine<IRulesEngine> rulesEngine;

public RulesEngine<IRulesEngine> RulesEngine
{
    get
    {
        if (null == rulesEngine)
        {
            string rulesPath = Path.Combine(Application.StartupPath, "Rules.cs");

            rulesEngine = new RulesEngine<IRulesEngine>(rulesPath, typeof(Rules).FullName);
        }

        return rulesEngine;
    }
}

public IRulesEngine RulesEngineInterface
{
    get { return RulesEngine.Interface; }
}

Pour votre exemple, vous voulez appeler Run (), alors je créerais une interface qui définit la méthode Run (), comme ceci:

public interface ITestRunner
{
    void Run();
}

Créez ensuite une classe qui l'implémente, comme ceci:

public class TestRunner : ITestRunner
{
    public void Run()
    {
        // implementation goes here
    }
}

Changez le nom de RulesEngine en quelque chose comme TestHarness et définissez vos propriétés:

private TestHarness<ITestRunner> testHarness;

public TestHarness<ITestRunner> TestHarness
{
    get
    {
        if (null == testHarness)
        {
            string sourcePath = Path.Combine(Application.StartupPath, "TestRunner.cs");

            testHarness = new TestHarness<ITestRunner>(sourcePath , typeof(TestRunner).FullName);
        }

        return testHarness;
    }
}

public ITestRunner TestHarnessInterface
{
    get { return TestHarness.Interface; }
}

Ensuite, où que vous souhaitiez l'appeler, vous pouvez simplement exécuter:

ITestRunner testRunner = TestHarnessInterface;

if (null != testRunner)
{
    testRunner.Run();
}

Cela fonctionnerait probablement très bien pour un système de plug-in, mais mon code tel quel est limité au chargement et à l'exécution d'un fichier, car toutes nos règles sont dans un fichier source C #. Je pense qu'il serait assez facile de le modifier pour simplement passer dans le fichier type/source de chaque fichier que vous voulez exécuter, cependant. Il vous suffirait de déplacer le code du getter vers une méthode prenant ces deux paramètres.

Utilisez également votre IRunnable à la place de ITestRunner.

11
Chris Doggett

Vous devrez utiliser la réflexion pour obtenir le type "TestRunner". Utilisez la méthode Assembly.GetType.

class Program
{
    static void Main(string[] args)
    {
        Assembly assembly = Assembly.LoadFile(@"C:\dyn.dll");
        Type type = Assembly.GetType("TestRunner");
        var obj = (TestRunner)Activator.CreateInstance(type);
        obj.Run();
    }
}
5
William Edmondson

Lorsque vous construisez votre assemblée, vous pouvez appeler AssemblyBuilder.SetEntryPoint , puis récupérez-le du Assembly.EntryPoint propriété pour l'invoquer.

N'oubliez pas que vous voudrez utiliser cette signature et notez qu'il n'est pas nécessaire qu'elle s'appelle Main:

static void Run(string[] args)
2
Sam Harwell