Правильний спосіб завантаження методу складання, пошуку класу та виклику ()


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 () лише на об'єкті? Як повинен виглядати цей код?

ОНОВЛЕННЯ: Вільям Едмондсон - див. Коментар


Якщо говорити про майбутнє ... Ви працювали з МЕФ? Давайте ви exportі importкласи в окремих збірках, що походять з відомого інтерфейсу
RJB

Відповіді:


78

Використовуйте AppDomain

Безпечніше та гнучкіше завантажувати збірку у свою 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 .


1
Що робити, якщо TypeIWantToLoad - це рядок? Чи є у вас альтернатива asm.GetType ("рядок типу") попередньої відповіді?
paz

2
Я думаю, що CreateInstanceFromAndUnwrapпотрібне AssemblyName, а не шлях; ти мав на увазі CreateFrom(path, fullname).Unwrap()? Також мене спалила MarshalByRefObjectвимога
drzaus 03.03.15

1
Можливо CreateInstanceAndUnwrap(typeof(TypeIWantToLoad).Assembly.FullName, typeof(TypeIWantToLoad).FullName)?
зникає

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

Я не згадую специфіку (і я на деякий час від’їжджаю від комп’ютера), але я вважаю, що Асамблею можна завантажувати лише один раз на AppDomain - тому вам або доведеться створювати нові AppDomains для кожного екземпляра Асамблеї ( і завантажте в них збірки), або вам доведеться перезапустити програму, перш ніж зможете скомпілювати нову версію збірки.
Джефф Стернал,

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

Дякую. Значення @ перед іменем змінної використовується, коли ім'я змінної є ключовим словом. Ви не можете назвати змінну "клас", "інтерфейс", "новий" тощо ... Але ви можете, якщо додасте знак @. Можливо, це не має значення в моєму випадку з великим "Я", але спочатку це була внутрішня змінна з геттером і сеттером, перш ніж я перетворив її на автоматичну властивість.
Chris Doggett,

Це правильно. Я забув про річ @. Як би ви впорались із запитанням, яке мені довелося зробити Джеффу Стерналю щодо "справи на згадку"? Я думаю, моя головна проблема зараз полягає в тому, що я можу створити динамічний .dll і завантажити його, але я можу зробити це лише один раз. Не знаю, як "розвантажити" збірку. Чи можна створити інший AppDomain, завантажити збірку в цьому просторі, використовувати його, а потім зняти цей другий AppDomain. Промити. Повторити.?
BuddyJoe

1
Неможливо розвантажити збірку, якщо ви не використовуєте другий AppDomain. Я не впевнений, як CS-Script робить це внутрішньо, але частина мого механізму правил, яку я видалив, - це FileSystemWatcher, який автоматично запускає LoadRules () знову, коли файл змінюється. Ми редагуємо правила, передаємо їх користувачам, чий клієнт перезаписує цей файл, FileSystemWatcher помічає зміни, перекомпілює та перезавантажує DLL, записуючи інший файл у тимчасовій директорії. Коли клієнт запускається, він очищає цей каталог до першої динамічної компіляції, тому у нас немає тонни залишків.
Chris Doggett

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, а потім "provider.CompileAssemblyFromSource"
BuddyJoe
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.