Prawidłowy sposób ładowania zestawu, znajdowania klasy i wywoływania metody Run ()


81

Przykładowy program konsoli.

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

    }
}

Chcę dynamicznie zbudować zestaw (.dll), a następnie załadować zestaw, utworzyć wystąpienie klasy i wywołać metodę Run () tej klasy. Czy powinienem spróbować przesłać na coś klasę TestRunner? Nie jestem pewien, jak typy w jednym zestawie (kod dynamiczny) będą wiedzieć o moich typach w mojej (statycznej aplikacji zestawu / powłoki). Czy lepiej jest po prostu użyć kilku wierszy kodu odbicia, aby wywołać Run () tylko na obiekcie? Jak powinien wyglądać ten kod?

AKTUALIZACJA: William Edmondson - patrz komentarz


Mówiąc z przyszłości ... czy pracowałeś z MEF? Niech ty exporti importklasy w oddzielnych zestawach wywodzących się ze znanego interfejsu
RJB

Odpowiedzi:


78

Użyj AppDomain

Bezpieczniejsze i bardziej elastyczne jest ładowanie zespołu jako AppDomainpierwszego.

Więc zamiast odpowiedzi udzielonej wcześniej :

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

Zasugerowałbym co następuje (dostosowane na podstawie tej odpowiedzi do pokrewnego pytania ):

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

Teraz możesz zwolnić zestaw i mieć różne ustawienia zabezpieczeń.

Jeśli chcesz jeszcze większej elastyczności i możliwości dynamicznego ładowania i zwalniania zestawów, powinieneś przyjrzeć się strukturze zarządzanych dodatków (tj. System.AddInPrzestrzeni nazw). Aby uzyskać więcej informacji, zobacz ten artykuł dotyczący dodatków i rozszerzalności w witrynie MSDN .


1
Co się stanie, jeśli TypeIWantToLoad jest ciągiem? Czy masz alternatywę dla asm.GetType poprzedniej odpowiedzi („ciąg typu”)?
paz

2
Myślę, że CreateInstanceFromAndUnwrapwymaga raczej AssemblyName niż ścieżki; pan miał na myśli CreateFrom(path, fullname).Unwrap()? Spalił mnie też MarshalByRefObjectwymóg
drzaus

1
Może CreateInstanceAndUnwrap(typeof(TypeIWantToLoad).Assembly.FullName, typeof(TypeIWantToLoad).FullName)?
fadden

1
Cześć ludzie, myślę, że mylisz CreateInstanceAndUnwrap z CreateInstanceFromAndUnwrap.
cdiggins

48

Jeśli nie masz dostępu do TestRunnerinformacji o typie w zestawie wywołującym (wygląda na to, że nie możesz), możesz wywołać metodę w następujący sposób:

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

Jeśli masz dostęp do IRunnabletypu interfejsu, możesz rzutować na niego swoje wystąpienie (zamiast TestRunnertypu, który jest zaimplementowany w dynamicznie tworzonym lub ładowanym zestawie, prawda?):

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

+1 Działało przy użyciu linii type.invokeMember. Powinienem użyć tej metody, czy dalej próbować coś zrobić z interfejsem? Wolałbym nawet nie martwić się umieszczaniem tego w dynamicznie budowanym kodzie.
BuddyJoe,

Hmm, czy drugi blok kodu nie działa dla Ciebie? Czy Twój zespół wywołujący ma dostęp do typu IRunnable?
Jeff Sternal,

Drugi blok działa. Wywołanie assemblera tak naprawdę nie wie o IRunnable. Więc myślę, że będę trzymał się drugiej metody. Nieznaczna kontynuacja. Kiedy regeneruję kod, a następnie ponownie wykonuję plik dyn.dll, nie mogę go zastąpić, ponieważ jest używany. Coś jak Assembly.UnloadType lub coś, co pozwoli mi zastąpić .dll? A może powinienem zrobić to „w pamięci”? myśli? dzięki
BuddyJoe

Chyba nie znam właściwego sposobu na zrobienie rzeczy „w pamięci”, jeśli to najlepsze rozwiązanie.
BuddyJoe,

Nie pamiętam szczegółów (i na chwilę odchodzę od mojego komputera), ale uważam, że Assembly można załadować tylko raz na AppDomain - więc albo będziesz musiał utworzyć nowe AppDomains dla każdej instancji Assembly ( i załaduj zestawy do nich) lub będziesz musiał ponownie uruchomić aplikację, zanim będzie można skompilować nową wersję zestawu.
Jeff Sternal,

12

Robię dokładnie to, czego szukasz w moim silniku reguł, który używa CS-Script do dynamicznego kompilowania, ładowania i uruchamiania C #. Powinien być łatwy do przetłumaczenia na to, czego szukasz, a ja podam przykład. Najpierw kod (okrojony):

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

Spowoduje to pobranie interfejsu typu T, skompilowanie pliku .cs do zestawu, utworzenie instancji klasy danego typu i wyrównanie tej instancji klasy do interfejsu T. Zasadniczo musisz tylko upewnić się, że klasa, której instancja została utworzona, implementuje ten interfejs. Używam właściwości, aby skonfigurować i uzyskać dostęp do wszystkiego, na przykład:

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

Na przykład chcesz wywołać Run (), więc stworzyłbym interfejs, który definiuje metodę Run (), na przykład:

public interface ITestRunner
{
    void Run();
}

Następnie utwórz klasę, która ją implementuje, na przykład:

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

Zmień nazwę RulesEngine na coś w rodzaju TestHarness i ustaw właściwości:

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

Następnie, gdziekolwiek chcesz to nazwać, możesz po prostu uruchomić:

ITestRunner testRunner = TestHarnessInterface;

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

Prawdopodobnie działałby świetnie dla systemu wtyczek, ale mój kod w obecnej postaci ogranicza się do ładowania i uruchamiania jednego pliku, ponieważ wszystkie nasze reguły znajdują się w jednym pliku źródłowym C #. Myślę, że byłoby dość łatwo zmodyfikować go, aby po prostu przekazać plik typu / źródłowy dla każdego, który chcesz uruchomić. Wystarczy przenieść kod z metody pobierającej do metody, która przyjmuje te dwa parametry.

Ponadto użyj IRunnable zamiast ITestRunner.


co to jest @Interface? bardzo fajne pomysły. trzeba to w pełni strawić. +1
BuddyJoe,

bardzo interesujące Nie zdawałem sobie sprawy, że parser C # musiał spojrzeć na jeden znak przekazujący @, aby sprawdzić, czy jest to część nazwy zmiennej, czy ciąg znaków @ "".
BuddyJoe,

Dzięki. Znak @ przed nazwą zmiennej jest używany, gdy nazwa zmiennej jest słowem kluczowym. Nie możesz nazwać zmiennej „klasa”, „interfejs”, „nowa” itp. Ale możesz to zrobić, jeśli dodasz przedrostek @. Prawdopodobnie nie ma to znaczenia w moim przypadku z dużym „I”, ale pierwotnie była to zmienna wewnętrzna z funkcją pobierającą i ustawiającą, zanim przekonwertowałem ją na właściwość automatyczną.
Chris Doggett,

Zgadza się. Zapomniałem o rzeczy @. Jak poradzisz sobie z pytaniem, które zadałem Jeffowi Sternalowi w sprawie „rzeczy w pamięci”? Myślę, że teraz moim wielkim problemem jest to, że mogę zbudować dynamiczny plik .dll i załadować go, ale mogę to zrobić tylko raz. Nie wiem jak "rozładować" zespół. Czy jest możliwe utworzenie innej domeny AppDomain, załadowanie zestawu w tej przestrzeni, użycie jej, a następnie usunięcie tej drugiej domeny AppDomain. Wypłukać. Powtarzać.?
BuddyJoe,

1
Nie ma możliwości zwolnienia zestawu, chyba że używasz drugiej domeny AppDomain. Nie jestem pewien, jak CS-Script robi to wewnętrznie, ale część mojego silnika reguł, którą usunąłem, to FileSystemWatcher, który automatycznie uruchamia LoadRules () ponownie po każdej zmianie pliku. Edytujemy reguły, wypychamy je do użytkowników, których klient nadpisuje ten plik, FileSystemWatcher zauważa zmiany i ponownie kompiluje i ponownie ładuje bibliotekę DLL, zapisując inny plik w katalogu tymczasowym. Po uruchomieniu klient czyści ten katalog przed pierwszą dynamiczną kompilacją, więc nie mamy wielu resztek.
Chris Doggett

6

Będziesz musiał użyć odbicia, aby uzyskać typ „TestRunner”. Użyj metody 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();
    }
}

Czy nie brakuje w tym kroku, w którym uzyskasz odpowiedni MethodInfotyp i wezwanie Invoke? (Zrozumiałem pierwotne pytanie jako określające, że dzwoniący nie wie nic o danym typie.)
Jeff Sternal,

Brakuje Ci jednej rzeczy. Musisz rzutować obj na typ TestRunner. var obj = (TestRunner) Activator.CreateInstance (typ);
BFree

Wygląda na to, że Tyndall faktycznie buduje tę bibliotekę dll na wcześniejszym etapie. Ta implementacja zakłada, że ​​wie, że metoda Run () już istnieje i wie, że nie ma parametrów. Jeśli rzeczywiście są one nieznane, musiałby przeprowadzić nieco głębszą refleksję
William Edmondson

hmmm. TestRunner to klasa w moim dynamicznym kodzie pisanym. Więc ten statyczny kod w twoim przykładzie nie może rozwiązać TestRunner. Nie ma pojęcia, co to jest.
BuddyJoe,

@WilliamEdmondson jak możesz użyć "(TestRunner)" w kodzie, skoro nie ma tu odniesienia?
Antoops

2

Podczas budowania zestawu możesz wywołać AssemblyBuilder.SetEntryPoint, a następnie pobrać go z Assembly.EntryPointwłaściwości, aby go wywołać.

Pamiętaj, że będziesz chciał użyć tego podpisu i pamiętaj, że nie trzeba go nazywać Main:

static void Run(string[] args)

Co to jest AssemblyBuilder? Próbowałem CodeDomProvider, a następnie „provider.CompileAssemblyFromSource”
BuddyJoe,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.