アセンブリをロードし、クラスを検索し、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クラスを何かにキャストしてみるべきですか?1つのアセンブリ(動的コード)の型が、私の(静的アセンブリ/シェルアプリ)の型についてどのように認識されるかわかりません。数行のリフレクションコードを使用して、オブジェクトだけでRun()を呼び出す方がよいでしょうか。そのコードはどのように見えるべきですか?

更新:ウィリアムエドモンソン-コメントを参照


将来から言えば... MEFと一緒に仕事をしたことがありますか?レッツはあなただexportimport知られているインターフェイスから派生する別のアセンブリ内のクラス
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();

これで、アセンブリをアンロードして、さまざまなセキュリティ設定を行うことができます。

アセンブリの動的なロードとアンロードにさらに柔軟性とパワーが必要な場合は、Managed Add-ins Framework(つまりSystem.AddIn名前空間)を確認する必要があります。詳細については、MSDNのアドインと拡張性に関するこの記事を参照してください


1
TypeIWantToLoadが文字列の場合はどうなりますか?前の回答のasm.GetType( "type string")に代わるものはありますか?
paz 2013

2
パスではなくAssemblyNameCreateInstanceFromAndUnwrapが必要だと思います。どういう意味ですか?また、私は要件によってやけどを負いましたCreateFrom(path, fullname).Unwrap()MarshalByRefObject
drzaus 2015

1
たぶんCreateInstanceAndUnwrap(typeof(TypeIWantToLoad).Assembly.FullName, typeof(TypeIWantToLoad).FullName)
2016年

1
こんにちは皆さん、CreateInstanceAndUnwrapとCreateInstanceFromAndUnwrapを混同していると思います。
cdiggins 2016

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

+ 1type.invokeMember行を使用して機能しました。その方法を使用する必要がありますか、それともインターフェイスで何かをしようとし続ける必要がありますか?動的に構築されたコードにそれを入れることさえ心配する必要はありません。
BuddyJoe 2009

うーん、コードの2番目のブロックは機能しませんか?呼び出し側アセンブリはIRunnableタイプにアクセスできますか?
ジェフ胸骨

2番目のブロックは機能します。アセンブリを呼び出すことは、IRunnableについて実際には知りません。だから私は2番目の方法に固執すると思います。わずかなフォローアップ。コードを再生成してからdyn.dllをやり直すと、使用中のために置き換えることができないようです。Assembly.UnloadTypeのようなもの、または.dllを置き換えることができるものはありますか?それとも、「メモリ内」で実行する必要がありますか?考え?ありがとう
BuddyJoe 2009

それが最善の解決策である場合、「メモリ内」のことを行う適切な方法がわからないと思います。
BuddyJoe 2009

詳細は思い出せませんが(しばらくの間コンピューターから離れます)、アセンブリはAppDomainごとに1回しかロードできないと思います。そのため、アセンブリインスタンスごとに新しいAppDomainを作成する必要があります(アセンブリをそれらにロードします)または、新しいバージョンのアセンブリをコンパイルする前に、アプリケーションを再起動する必要があります。
ジェフ胸骨

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

プラグインシステムではおそらくうまく機能しますが、すべてのルールが1つのC#ソースファイルに含まれているため、コードはそのままで1つのファイルの読み込みと実行に制限されます。ただし、実行したいファイルごとにタイプ/ソースファイルを渡すように変更するのは非常に簡単だと思います。コードをゲッターから、これら2つのパラメーターを受け取るメソッドに移動するだけです。

また、ITestRunnerの代わりにIRunnableを使用してください。


@Interfaceとは何ですか?ここで非常にクールなアイデア。これを完全に消化する必要があります。+1
BuddyJoe 2009

非常に興味深いことに、C#パーサーは、変数名または@ ""文字列の一部であるかどうかを確認するために、@を1文字渡す必要があることに気づきませんでした。
BuddyJoe 2009

ありがとう。変数名がキーワードの場合、変数名の前の@が使用されます。変数に「class」、「interface」、「new」などの名前を付けることはできません。ただし、@を前に付けると名前を付けることができます。大文字の「I」を使用した場合はおそらく問題ではありませんが、自動プロパティに変換する前は、元々はゲッターとセッターを備えた内部変数でした。
Chris Doggett

そのとおり。@のことを忘れてしまいました。「記憶に残ること」についてジェフ・スターナルに私がしなければならなかった質問にどのように対処しますか?今の私の大きな問題は、ダイナミック.dllをビルドしてロードできることだと思いますが、それは1回しか実行できません。アセンブリを「アンロード」する方法がわかりません。別のAppDomainを作成して、そのスペースにアセンブリをロードして使用し、この2番目のAppDomainを削除することは可能ですか。リンス。繰り返す。?
BuddyJoe 2009

1
2番目の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か?(元の質問は、問題のタイプについて何も知らない発信者を指定するものとして理解しました。)
Jeff Sternal

あなたは一つのことを逃しています。TestRunnerと入力するには、objをキャストする必要があります。var obj =(TestRunner)Activator.CreateInstance(type);
bFree 2009

Tyndallが実際にこのdllを前のステップで構築しているようです。この実装は、メソッドRun()がすでに存在し、パラメーターがないことを知っていることを前提としています。これらが実際に不明な場合、彼はもう少し深く考察する必要があります
William Edmondson

うーん。TestRunnerは、動的に記述されたコード内のクラスです。したがって、この例の静的コードはTestRunnerを解決できません。それが何であるかはわかりません。
BuddyJoe 2009

@WilliamEdmondsonここでは参照されていないため、コードで「(TestRunner)」をどのように使用できますか?
Antoops 2018年

2

アセンブリをビルドするときに、を呼び出してAssemblyBuilder.SetEntryPoint、から元に戻すことができます。Assembly.EntryPointプロパティて呼び出すことができます。

この署名を使用する必要があることに注意してください。名前を付ける必要はないことに注意してくださいMain

static void Run(string[] args)

AssemblyBuilderとは何ですか?私はCodeDomProviderを試し、次に「provider.CompileAssemblyFromSource」を
試していました
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.