어셈블리를로드하고 클래스를 찾고 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 ()을 호출하기 위해 몇 줄의 리플렉션 코드를 사용하는 것이 더 낫습니까? 그 코드는 어떤 모습이어야합니까?

업데이트 : William Edmondson-코멘트 참조


미래에서 말하면 ... MEF와 함께 일했습니까? 하자 당신의 exportimport알려진 인터페이스에서 파생 별도의 어셈블리의 클래스
상기 R1b

답변:


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

이제 어셈블리를 언로드하고 다른 보안 설정을 가질 수 있습니다.

어셈블리의 동적로드 및 언로드를 위해 더 많은 유연성과 성능을 원한다면 Managed Add-ins Framework (즉, System.AddIn네임 스페이스)를 살펴 봐야합니다 . 자세한 내용은 MSDN의 추가 기능 및 확장성에 대한 이 문서를 참조하십시오 .


1
TypeIWantToLoad가 문자열이면 어떻게됩니까? 이전 답변의 asm.GetType ( "type string")에 대한 대안이 있습니까?
paz

2
경로가 아닌 AssemblyNameCreateInstanceFromAndUnwrap필요 하다고 생각 합니다 . 그랬어 ? 또한 나는에 의해 불에있어 요구 사항CreateFrom(path, fullname).Unwrap()MarshalByRefObject
drzaus

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 형식에 대한 액세스 권한이 있습니까?
Jeff Sternal

두 번째 블록이 작동합니다. 어셈블리 호출은 IRunnable에 대해 실제로 알지 못합니다. 그래서 두 번째 방법을 고수 할 것 같습니다. 약간의 후속 조치. 코드를 다시 생성 한 다음 dyn.dll을 다시 실행하면 사용 중이므로 대체 할 수없는 것 같습니다. Assembly.UnloadType 또는 .dll을 대체 할 수있는 것과 같은 것이 있습니까? 아니면 "메모리"에서해야합니까? 생각? 감사합니다
BuddyJoe

이것이 최선의 해결책이라면 "인 메모리"작업을 수행하는 적절한 방법을 모르겠습니다.
BuddyJoe

세부 사항은 기억 나지 않지만 (그리고 잠시 동안 컴퓨터에서 멀어 질 예정입니다.) 어셈블리는 AppDomain 당 한 번만로드 할 수 있다고 믿습니다. 따라서 각 어셈블리 인스턴스에 대해 새 AppDomain을 만들어야합니다 ( 어셈블리를로드하지 않으면 새 버전의 어셈블리를 컴파일하기 전에 애플리케이션을 다시 시작해야합니다.
Jeff Sternal

12

C #을 동적으로 컴파일,로드 및 실행하기 위해 CS-Script 를 사용하는 내 규칙 엔진에서 원하는 작업을 정확히 수행하고 있습니다 . 찾고있는 내용으로 쉽게 번역 할 수 있어야하며 예를 들어 보겠습니다. 첫째, 코드 (줄임) :

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 # 소스 파일에 있기 때문에있는 그대로의 코드는 하나의 파일을로드하고 실행하는 것으로 제한됩니다. 그래도 실행하려는 각 파일에 대해 유형 / 소스 파일을 전달하도록 수정하는 것이 매우 쉬울 것이라고 생각합니다. getter에서이 두 매개 변수를 사용하는 메소드로 코드를 이동하면됩니다.

또한 ITestRunner 대신 IRunnable을 사용하십시오.


@Interface는 무엇입니까? 여기에 아주 멋진 아이디어. 이것을 완전히 소화해야합니다. +1
BuddyJoe

매우 흥미로 웠습니다. C # 파서가 변수 이름의 일부인지 @ ""문자열인지 확인하기 위해 @를 전달하는 한 문자를 찾아야한다는 사실을 알지 못했습니다.
BuddyJoe

감사. 변수 이름이 키워드 인 경우 변수 이름 앞의 @가 사용됩니다. 변수에 "class", "interface", "new"등의 이름을 지정할 수 없습니다.하지만 앞에 @를 추가하면 가능합니다. 아마도 대문자 "I"가있는 제 경우에는 문제가되지 않지만 원래는 자동 속성으로 변환하기 전에 getter와 setter가있는 내부 변수였습니다.
Chris Doggett

맞습니다. @ 일을 잊어 버렸습니다. "기억 속 문제"에 대해 Jeff Sternal에게했던 질문을 어떻게 처리 하시겠습니까? 이제 내 큰 문제는 동적 .dll을 빌드하고로드 할 수 있지만 한 번만 수행 할 수 있다는 것입니다. 어셈블리를 "언로드"하는 방법을 모릅니다. 다른 AppDomain을 만들어 해당 공간에 어셈블리를로드하고 사용한 다음이 두 번째 AppDomain을 중단 할 수 있습니까? 헹구기. 반복.?
BuddyJoe

1
두 번째 AppDomain을 사용하지 않으면 어셈블리를 언로드 할 수 없습니다. CS-Script가 내부적으로 어떻게 수행하는지 잘 모르겠지만, 제가 제거한 규칙 엔진의 일부는 파일이 변경 될 때마다 자동으로 LoadRules ()를 다시 실행하는 FileSystemWatcher입니다. 규칙을 편집하고 클라이언트가 해당 파일을 덮어 쓰는 사용자에게 푸시하고 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 (type);
BFree

Tyndall이 실제로 이전 단계에서이 dll을 빌드하는 것처럼 들립니다. 이 구현에서는 Run () 메서드가 이미 존재하고 매개 변수가 없다는 것을 알고 있다고 가정합니다. 이 참 알 수없는 경우, 그는 좀 더 깊이 반영 할 필요가있을 것이다
윌리엄 에드 먼슨

흠. TestRunner는 동적으로 작성된 코드 내의 클래스입니다. 따라서 예제의이 정적 코드는 TestRunner를 해결할 수 없습니다. 그것이 무엇인지 전혀 모릅니다.
BuddyJoe

@WilliamEdmondson 여기서 참조되지 않는 코드에서 "(TestRunner)"를 어떻게 사용할 수 있습니까?
Antoops

2

어셈블리를 빌드 할 때를 호출 AssemblyBuilder.SetEntryPoint한 다음 Assembly.EntryPoint속성 에서 다시 가져 와서 호출 할 수 있습니다.

이 서명을 사용하고 싶을 것이며 이름을 지정할 필요가 없습니다 Main.

static void Run(string[] args)

AssemblyBuilder 란 무엇입니까? 나는 CodeDomProvider하고 "provider.CompileAssemblyFromSource"를 시도하고 있었다
BuddyJoe
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.