加载汇编、查找类和调用 Run()方法的正确方法

控制台程序示例。

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 类转换为其他类?不确定一个程序集(动态代码)中的类型如何知道我的(静态程序集/shell 应用程序)中的类型。仅仅使用几行反射代码在一个对象上调用 Run ()是否更好?代码应该是什么样子的?

更新: William Edmondson-参见评论

188351 次浏览

您需要使用反射来获得类型“ TestRunner”。

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

如果不能访问调用程序集中的 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();

生成程序集时,可以调用 AssemblyBuilder.SetEntryPoint,然后从 Assembly.EntryPoint属性获取它以调用它。

请记住,您需要使用这个签名,并且注意它不一定要命名为 Main:

static void Run(string[] args)

我所做的正是您在我的规则引擎中所寻找的,它使用 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 # 源文件中。不过,我认为对它进行修改很容易,只需要为每个要运行的文件传入 type/source 文件。您只需将代码从 getter 移动到接受这两个参数的方法中。

另外,使用 IRunnable 代替 ITestRunner。

使用 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 上的外接程序和扩展性的本文。