Richtige Methode zum Laden der Assembly, Suchen der Klasse und Aufrufen der Run () -Methode

81

Beispiel für ein Konsolenprogramm.

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();

    }
}

Ich möchte dynamisch eine Assembly (.dll) erstellen und dann die Assembly laden, eine Klasse instanziieren und die Run () -Methode dieser Klasse aufrufen. Sollte ich versuchen, die TestRunner-Klasse auf etwas zu übertragen? Ich bin mir nicht sicher, wie die Typen in einer Assembly (dynamischer Code) über meine Typen in meiner (statische Assembly / Shell-App) Bescheid wissen würden. Ist es besser, nur ein paar Zeilen Reflektionscode zu verwenden, um Run () nur für ein Objekt aufzurufen? Wie sollte dieser Code aussehen?

UPDATE: William Edmondson - siehe Kommentar

BuddyJoe
quelle
Apropos Zukunft ... haben Sie mit MEF zusammengearbeitet? Lassen Sie uns exportund importKlassen in separaten Baugruppen von einer bekannten Schnittstelle ableiten
RJB

Antworten:

77

Verwenden Sie eine AppDomain

Es ist sicherer und flexibler, die Baugruppe selbst zu laden AppDomain .

Also anstelle der zuvor gegebenen Antwort :

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();

Ich würde Folgendes vorschlagen (angepasst aus dieser Antwort auf eine verwandte Frage ):

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();

Jetzt können Sie die Baugruppe entladen und verschiedene Sicherheitseinstellungen vornehmen.

Wenn Sie noch mehr Flexibilität und Leistung für das dynamische Laden und Entladen von Assemblys wünschen, sollten Sie sich das Managed Add-Ins Framework (dh den System.AddInNamespace) ansehen . Weitere Informationen finden Sie in diesem Artikel zu Add-Ins und Erweiterbarkeit auf MSDN .

cdiggins
quelle
1
Was ist, wenn TypeIWantToLoad eine Zeichenfolge ist? Haben Sie eine Alternative zum asm.GetType der vorherigen Antwort ("type string")?
Paz
2
Ich denke, CreateInstanceFromAndUnwraperfordert den AssemblyName eher als einen Pfad; meinst du CreateFrom(path, fullname).Unwrap()? Auch ich wurde von der MarshalByRefObjectAnforderung
drzaus
1
Vielleicht CreateInstanceAndUnwrap(typeof(TypeIWantToLoad).Assembly.FullName, typeof(TypeIWantToLoad).FullName)?
Fadden
1
Hallo Leute, ich glaube, Sie verwechseln CreateInstanceAndUnwrap mit CreateInstanceFromAndUnwrap.
Cdiggins
48

Wenn Sie keinen Zugriff auf die TestRunnerTypinformationen in der aufrufenden Assembly haben (dies scheint nicht der Fall zu sein), können Sie die Methode folgendermaßen aufrufen:

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);

Wenn Sie Zugriff auf den IRunnableSchnittstellentyp haben, können Sie Ihre Instanz in diesen umwandeln (und nicht in den TestRunnerTyp, der in der dynamisch erstellten oder geladenen Assembly implementiert ist, oder?):

  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();
Jeff Sternal
quelle
+1 Es hat mit der Zeile type.invokeMember funktioniert. Sollte ich diese Methode verwenden oder weiterhin versuchen, etwas mit der Schnittstelle zu tun? Ich möchte mich lieber nicht einmal darum kümmern müssen, das in den dynamisch erstellten Code aufzunehmen.
BuddyJoe
Hmm, funktioniert der zweite Codeblock nicht für Sie? Hat Ihre aufrufende Assembly Zugriff auf den Typ IRunnable?
Jeff Sternal
Der zweite Block funktioniert. Das Aufrufen der Assembly weiß nicht wirklich über IRunnable Bescheid. Also werde ich wohl bei der zweiten Methode bleiben. Leichte Nachverfolgung. Wenn ich den Code neu generiere und dann dyn.dll wiederhole, kann ich ihn scheinbar nicht ersetzen, da er verwendet wird. Etwas wie ein Assembly.UnloadType oder etwas, mit dem ich die DLL ersetzen kann? Oder sollte ich es nur "im Gedächtnis" tun? Gedanken? danke
BuddyJoe
Ich schätze, ich weiß nicht, wie ich das "In Memory" -Ding richtig machen soll, wenn das die beste Lösung ist.
BuddyJoe
Ich erinnere mich nicht an die Details (und ich entferne mich für eine Weile von meinem Computer), aber ich glaube, dass eine Assembly nur einmal pro AppDomain geladen werden kann - daher müssen Sie entweder für jede Assembly-Instanz neue AppDomains erstellen ( und laden Sie die Assemblys in diese) oder Sie müssen Ihre Anwendung neu starten, bevor Sie eine neue Version der Assembly kompilieren können.
Jeff Sternal
12

Ich mache genau das, wonach Sie in meiner Regel-Engine suchen, die verwendet CS-Script zum dynamischen Kompilieren, Laden und Ausführen von C # verwendet. Es sollte leicht in das zu übersetzen sein, wonach Sie suchen, und ich werde ein Beispiel geben. Zuerst der Code (abgespeckte):

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;
        }
    }
}

Dies nimmt eine Schnittstelle vom Typ T, kompiliert eine CS-Datei in eine Assembly, instanziiert eine Klasse eines bestimmten Typs und richtet diese instanziierte Klasse an der T-Schnittstelle aus. Grundsätzlich müssen Sie nur sicherstellen, dass die instanziierte Klasse diese Schnittstelle implementiert. Ich benutze Eigenschaften, um alles einzurichten und darauf zuzugreifen, wie folgt:

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; }
}

In Ihrem Beispiel möchten Sie Run () aufrufen, daher würde ich eine Schnittstelle erstellen, die die Run () -Methode wie folgt definiert:

public interface ITestRunner
{
    void Run();
}

Erstellen Sie dann eine Klasse, die sie wie folgt implementiert:

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

Ändern Sie den Namen von RulesEngine in TestHarness und legen Sie Ihre Eigenschaften fest:

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; }
}

Dann können Sie überall dort, wo Sie es aufrufen möchten, einfach Folgendes ausführen:

ITestRunner testRunner = TestHarnessInterface;

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

Es würde wahrscheinlich gut für ein Plugin-System funktionieren, aber mein Code ist so wie er ist auf das Laden und Ausführen einer Datei beschränkt, da sich alle unsere Regeln in einer C # -Quelldatei befinden. Ich würde denken, dass es ziemlich einfach wäre, es so zu ändern, dass nur die Typ- / Quelldatei für jede Datei übergeben wird, die Sie ausführen möchten. Sie müssten nur den Code aus dem Getter in eine Methode verschieben, die diese beiden Parameter verwendet.

Verwenden Sie Ihr IRunnable auch anstelle von ITestRunner.

Chris Doggett
quelle
Was ist das @Interface? sehr coole ideen hier. müssen dies vollständig verdauen. +1
BuddyJoe
Sehr interessant Ich wusste nicht, dass der C # -Parser ein Zeichen nach dem @ suchen musste, um zu sehen, ob es Teil eines Variablennamens oder einer @ "" - Zeichenfolge war.
BuddyJoe
Vielen Dank. Das @ vor dem Variablennamen wird verwendet, wenn der Variablenname ein Schlüsselwort ist. Sie können eine Variable nicht "Klasse", "Schnittstelle", "Neu" usw. benennen. Sie können dies jedoch, wenn Sie ein @ voranstellen. Wahrscheinlich spielt es in meinem Fall mit einem Großbuchstaben "I" keine Rolle, aber es war ursprünglich eine interne Variable mit einem Getter und einem Setter, bevor ich sie in eine Auto-Eigenschaft konvertierte.
Chris Doggett
Das stimmt. Ich habe das @ Ding vergessen. Wie würden Sie mit der Frage umgehen, die ich an Jeff Sternal bezüglich der "In-Memory-Sache" hatte? Ich denke, mein großes Problem ist jetzt, dass ich die dynamische DLL erstellen und laden kann, aber ich kann es nur einmal tun. Ich weiß nicht, wie ich die Baugruppe "entladen" soll. Ist es möglich, eine andere AppDomain zu erstellen? Laden Sie die Assembly in diesen Bereich, verwenden Sie sie und entfernen Sie diese zweite AppDomain. Spülen. Wiederholen.?
BuddyJoe
1
Es gibt keine Möglichkeit, die Assembly zu entladen, es sei denn, Sie verwenden eine zweite AppDomain. Ich bin nicht sicher, wie CS-Script dies intern macht, aber der Teil meiner Regelengine, den ich entfernt habe, ist ein FileSystemWatcher, der LoadRules () automatisch erneut ausführt, wenn sich die Datei ändert. Wir bearbeiten die Regeln, geben sie an die Benutzer weiter, deren Client diese Datei überschreibt, der FileSystemWatcher bemerkt die Änderungen und kompiliert die DLL neu und lädt sie neu, indem er eine andere Datei in das temporäre Verzeichnis schreibt. Wenn der Client gestartet wird, wird dieses Verzeichnis vor der ersten dynamischen Kompilierung gelöscht, sodass wir nicht viele Reste haben.
Chris Doggett
6

Sie müssen Reflection verwenden, um den Typ "TestRunner" zu erhalten. Verwenden Sie die Assembly.GetType-Methode.

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();
    }
}
William Edmondson
quelle
Fehlt dabei nicht ein Schritt, in dem Sie den entsprechenden MethodInfoTyp und Anruf erhalten Invoke? (Ich verstand die ursprüngliche Frage als Angabe des Anrufers, der nichts über den fraglichen Typ wusste.)
Jeff Sternal
Ihnen fehlt eine Sache. Sie müssen obj umsetzen, um TestRunner einzugeben. var obj = (TestRunner) Activator.CreateInstance (Typ);
BFree
Es hört sich so an, als würde Tyndall diese DLL tatsächlich in einem früheren Schritt erstellen. Diese Implementierung setzt voraus, dass er weiß, dass die Methode Run () bereits vorhanden ist und keine Parameter hat. Wenn diese tatsächlich unbekannt sind, müsste er etwas tiefer nachdenken
William Edmondson
Hmmm. TestRunner ist eine Klasse in meinem dynamisch geschriebenen Code. Dieser statische Code in Ihrem Beispiel kann TestRunner also nicht auflösen. Es hat keine Ahnung, was es ist.
BuddyJoe
@WilliamEdmondson Wie können Sie "(TestRunner)" im Code verwenden, da hier nicht darauf verwiesen wird?
Antoops
2

Wenn Sie Ihre Assembly erstellen, können Sie sie aufrufen AssemblyBuilder.SetEntryPointund dann von der Assembly.EntryPointEigenschaft zurückholen , um sie aufzurufen.

Denken Sie daran, dass Sie diese Signatur verwenden möchten, und beachten Sie, dass sie nicht benannt werden muss Main:

static void Run(string[] args)
Sam Harwell
quelle
Was ist AssemblyBuilder? Ich habe versucht, CodeDomProvider und dann "provider.CompileAssemblyFromSource"
BuddyJoe