Правильный способ загрузки сборки, поиска класса и вызова метода Run ()

81

Пример консольной программы.

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

    }
}

Я хочу динамически построить сборку (.dll), а затем загрузить сборку, создать экземпляр класса и вызвать метод Run () этого класса. Стоит ли мне попробовать привести к чему-нибудь класс TestRunner? Не уверен, как типы в одной сборке (динамический код) узнают о моих типах в моем (статическом приложении сборки / оболочки). Не лучше ли использовать несколько строк кода отражения для вызова Run () только для объекта? Как должен выглядеть этот код?

ОБНОВЛЕНИЕ: Уильям Эдмондсон - см. Комментарий

BuddyJoe
источник
Говоря о будущем ... вы работали с MEF? Давайте вы exportи importклассы в отдельных сборках, производных от известного интерфейса
RJB

Ответы:

78

Использовать домен приложения

Безопаснее и гибче AppDomainсначала загружать сборку в отдельную .

Итак, вместо ответа, данного ранее :

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

Я бы предложил следующее (адаптировано из этого ответа на связанный вопрос ):

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

Теперь вы можете выгрузить сборку и иметь другие настройки безопасности.

Если вам нужна еще большая гибкость и мощность для динамической загрузки и выгрузки сборок, вам следует взглянуть на структуру управляемых надстроек (т.е. System.AddInпространство имен). Дополнительные сведения см. В этой статье о надстройках и расширяемости в MSDN .

cdiggins
источник
1
Что, если TypeIWantToLoad - это строка? Есть ли у вас альтернатива asm.GetType ("строка типа") из предыдущего ответа?
paz
2
Я думаю, что CreateInstanceFromAndUnwrapтребуется AssemblyName, а не путь; ты имел ввиду CreateFrom(path, fullname).Unwrap()? Также меня обожгло MarshalByRefObjectтребование
drzaus 03
1
Возможно CreateInstanceAndUnwrap(typeof(TypeIWantToLoad).Assembly.FullName, typeof(TypeIWantToLoad).FullName)?
fadden
1
Привет, ребята, я считаю, что вы путаете CreateInstanceAndUnwrap с CreateInstanceFromAndUnwrap.
cdiggins
48

Если у вас нет доступа к TestRunnerинформации о типе в вызывающей сборке (похоже, что у вас ее нет), вы можете вызвать метод следующим образом:

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

Если у вас есть доступ к IRunnableтипу интерфейса, вы можете привести к нему свой экземпляр (а не TestRunnerтип, который реализован в динамически созданной или загруженной сборке, верно?):

  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 Это работало с использованием строки type.invokeMember. Следует ли мне использовать этот метод или продолжать пытаться что-то сделать с интерфейсом? Я бы предпочел не беспокоиться об этом в динамически создаваемом коде.
BuddyJoe
Хм, а второй блок кода вам не подходит? Имеет ли ваша вызывающая сборка доступ к типу IRunnable?
Джефф Стернал,
Второй блок работает. Вызывающая сборка на самом деле ничего не знает о IRunnable. Думаю, я буду придерживаться второго метода. Незначительное продолжение. Когда я регенерирую код, а затем переделываю dyn.dll, мне кажется, что я не могу его заменить, потому что он уже используется. Что-нибудь вроде Assembly.UnloadType или что-то, что позволит мне заменить .dll? Или надо просто делать это «по памяти»? мысли? спасибо
BuddyJoe
Думаю, я не знаю, как правильно делать "в памяти", если это лучшее решение.
BuddyJoe
Я не помню подробностей (и на некоторое время ухожу от компьютера), но я считаю, что сборка может быть загружена только один раз для каждого домена приложения, поэтому вам придется либо создавать новые домены приложений для каждого экземпляра сборки ( и Загрузите сборки в них), или вам придется перезапустить приложение, прежде чем вы сможете скомпилировать новую версию сборки.
Джефф Стернал,
12

Я делаю именно то, что вы ищете, в моем движке правил, который использует CS-Script для динамической компиляции, загрузки и запуска C #. Он должен легко переводиться в то, что вы ищете, и я приведу пример. Во-первых, код (урезанный):

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

Это примет интерфейс типа T, скомпилирует файл .cs в сборку, создаст экземпляр класса данного типа и выровняет этот экземпляр класса с интерфейсом T. По сути, вам просто нужно убедиться, что созданный класс реализует этот интерфейс. Я использую свойства для настройки и доступа ко всему, например:

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

В вашем примере вы хотите вызвать Run (), поэтому я бы создал интерфейс, определяющий метод Run (), например:

public interface ITestRunner
{
    void Run();
}

Затем создайте класс, который его реализует, например:

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

Измените имя RulesEngine на что-то вроде TestHarness и установите свои свойства:

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

Затем, где бы вы ни захотели его вызвать, вы можете просто запустить:

ITestRunner testRunner = TestHarnessInterface;

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

Это, вероятно, отлично подойдет для системы плагинов, но мой код как есть ограничен загрузкой и запуском одного файла, поскольку все наши правила находятся в одном исходном файле C #. Я бы подумал, что было бы довольно легко изменить его, просто передав тип / исходный файл для каждого из них, который вы хотите запустить. Вам просто нужно переместить код из получателя в метод, который принимает эти два параметра.

Кроме того, используйте IRunnable вместо ITestRunner.

Крис Доггетт
источник
что такое @Interface? здесь очень крутые идеи. нужно это полностью переварить. +1
BuddyJoe
очень интересно. Я не понимал, что синтаксический анализатор C # должен искать один символ, передающий @, чтобы увидеть, является ли он частью имени переменной или строкой @ "".
BuddyJoe
Благодарю. Символ @ перед именем переменной используется, когда имя переменной является ключевым словом. Вы не можете назвать переменную "class", "interface", "new" и т. Д. Но вы можете, если вы добавите @. Вероятно, не имеет значения в моем случае с заглавной «I», но изначально это была внутренняя переменная с геттером и сеттером, прежде чем я преобразовал ее в автоматическое свойство.
Крис Доггетт
Это правильно. Я забыл про @ вещь. Как бы вы отреагировали на вопрос, который я задал Джеффу Стерналу о «вещах в памяти»? Думаю, сейчас моя большая проблема в том, что я могу создать динамическую .dll и загрузить ее, но сделать это можно только один раз. Не знаю как "разгрузить" сборку. Можно ли создать другой домен приложений, загрузить сборку в этом пространстве, использовать ее, а затем отключить этот второй домен приложений. Промыть. Повторение.?
BuddyJoe
1
Невозможно выгрузить сборку, если вы не используете второй домен приложения. Я не уверен, как CS-Script делает это внутри, но часть моего движка правил, которую я исключил, - это FileSystemWatcher, который автоматически запускает LoadRules () при каждом изменении файла. Мы редактируем правила, передаем их пользователям, чей клиент перезаписывает этот файл, FileSystemWatcher замечает изменения, перекомпилирует и перезагружает DLL, записывая другой файл во временный каталог. Когда клиент запускается, он очищает этот каталог перед первой динамической компиляцией, поэтому у нас не остается тонны остатков.
Крис Доггетт,
6

Вам нужно будет использовать отражение, чтобы получить тип TestRunner. Используйте метод 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();
    }
}
Уильям Эдмондсон
источник
Разве здесь не пропущен шаг, на котором вы получаете соответствующий MethodInfoтип и вызов Invoke? (Я понял исходный вопрос как указание на то, что вызывающий абонент ничего не знает о рассматриваемом
Типе
Вы упускаете одну вещь. Вы должны привести obj к типу TestRunner. var obj = (TestRunner) Activator.CreateInstance (тип);
BFree
Похоже, Тиндаль на самом деле создает эту dll на более раннем этапе. Эта реализация предполагает, что он знает, что метод Run () уже существует, и знает, что у него нет параметров. Если они действительно неизвестны, ему нужно будет глубже поразмыслить,
Уильям Эдмондсон
хммм. TestRunner - это класс внутри моего динамически написанного кода. Таким образом, этот статический код в вашем примере не может разрешить TestRunner. Он понятия не имеет, что это такое.
BuddyJoe
@WilliamEdmondson, как вы можете использовать "(TestRunner)" в коде, если здесь нет ссылок?
Antoops
2

Когда вы создаете свою сборку, вы можете вызвать AssemblyBuilder.SetEntryPoint, а затем получить ее обратно из Assembly.EntryPointсвойства, чтобы вызвать ее.

Имейте в виду, что вы захотите использовать эту подпись, и обратите внимание, что ее имя необязательно Main:

static void Run(string[] args)
Сэм Харвелл
источник
Что такое AssemblyBuilder? Я пробовал CodeDomProvider, а затем "providerr.CompileAssemblyFromSource"
BuddyJoe