【问题标题】:Correct Way to Load Assembly, Find Class and Call Run() Method加载程序集、查找类和调用 Run() 方法的正确方法
【发布时间】:2010-11-11 09:18:11
【问题描述】:

示例控制台程序。

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() 会更好吗?这段代码应该是什么样子的?

更新: 威廉埃德蒙森 - 见评论

【问题讨论】:

  • 从未来说起……您与 MEF 合作过吗?让您在派生自已知接口的单独程序集中的 exportimport

标签: c# .net reflection


【解决方案1】:

使用 AppDomain

先将程序集加载到自己的AppDomain中更安全、更灵活。

所以不是the answer given previously:

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

我建议如下(改编自this answer to a related question):

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 命名空间)。如需更多信息,请参阅Add-ins and Extensibility on MSDN 上的这篇文章。

【讨论】:

  • 如果 TypeIWantToLoad 是字符串怎么办?你有替代上一个答案的 asm.GetType("type string") 吗?
  • 我认为 CreateInstanceFromAndUnwrap 需要 AssemblyName 而不是路径;你的意思是CreateFrom(path, fullname).Unwrap()?我也被MarshalByRefObject requirement烫伤了
  • 或许CreateInstanceAndUnwrap(typeof(TypeIWantToLoad).Assembly.FullName, typeof(TypeIWantToLoad).FullName)?
  • 大家好,我相信您将 CreateInstanceAndUnwrap 与 CreateInstanceFromAndUnwrap 混淆了。
【解决方案2】:

如果你无法访问调用程序集中的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 行。我应该使用该方法还是继续尝试对界面做一些事情?我宁愿不必担心将其放入动态构建的代码中。
  • 嗯,第二段代码不适合你吗?您的调用程序集是否有权访问 IRunnable 类型?
  • 第二块确实有效。调用程序集并不真正了解 IRunnable。所以我想我会坚持第二种方法。轻微跟进。当我重新生成代码然后重做 dyn.dll 时,我似乎无法替换它,因为它正在使用中。像 Assembly.UnloadType 之类的东西可以让我替换 .dll 吗?还是我应该“在记忆中”这样做?想法?谢谢
  • 如果这是最好的解决方案,我想我不知道做“内存中”事情的正确方法。
  • 我不记得具体细节(我暂时离开我的电脑),但我相信每个 AppDomain 只能加载一次程序集 - 所以你要么必须为每个程序集实例创建新的 AppDomain(并将程序集加载到其中),否则您必须重新启动应用程序才能编译新版本的程序集。
【解决方案3】:

我正在做的正是您在我的规则引擎中寻找的东西,它使用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# 源文件中。不过,我认为将其修改为只为您要运行的每个文件传递类型/源文件会很容易。您只需将代码从 getter 移动到采用这两个参数的方法中。

另外,使用您的 IRunnable 代替 ITestRunner。

【讨论】:

  • @Interface 是什么?这里有非常酷的想法。需要充分消化这一点。 +1
  • 非常有趣,我没有意识到 C# 解析器必须通过 @ 来查看一个字符,以确定它是变量名的一部分还是 @"" 字符串的一部分。
  • 谢谢。当变量名是关键字时,使用变量名前的@。您不能将变量命名为“类”、“接口”、“新”等……但如果您在前面加上 @,则可以。在我使用大写“I”的情况下可能无关紧要,但在我将其转换为自动属性之前,它最初是一个带有 getter 和 setter 的内部变量。
  • 没错。我忘记了@的事情。你会如何处理我向 Jeff Sternal 提出的关于“记忆中的事情”的问题?我想我现在的大问题是我可以构建动态 .dll 并加载它,但我只能执行一次。不知道如何“卸载”程序集。是否可以创建另一个 AppDomain 加载该空间中的程序集,使用它,然后取下第二个 AppDomain。冲洗。重复。?
  • 除非您使用第二个 AppDomain,否则无法卸载程序集。我不确定 CS-Script 是如何在内部执行它的,但是我剥离的规则引擎的一部分是 FileSystemWatcher,它会在文件更改时再次自动运行 LoadRules()。我们编辑规则,将它们推送给用户,用户的客户端覆盖该文件,FileSystemWatcher 注意到更改,并通过在 temp 目录中写入另一个文件来重新编译和重新加载 DLL。当客户端启动时,它会在第一次动态编译之前清除该目录,因此我们没有大量剩余。
【解决方案4】:

您将需要使用反射来获取类型“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);
  • 听起来 Tyndall 实际上是在更早的步骤中构建此 dll。这个实现假设他知道方法 Run() 已经存在并且知道它没有参数。如果这些确实是未知的,那么他需要做一些更深入的反思
  • 嗯。 TestRunner 是我的动态编写代码中的一个类。因此,您示例中的此静态代码无法解析 TestRunner。它不知道它是什么。
  • @WilliamEdmondson 如何在代码中使用“(TestRunner)”,因为这里没有引用它?
【解决方案5】:

在构建程序集时,可以调用AssemblyBuilder.SetEntryPoint,然后从Assembly.EntryPoint 属性中取回它以调用它。

请记住,您需要使用此签名,并注意不必将其命名为 Main

static void Run(string[] args)

【讨论】:

  • 什么是 AssemblyBuilder?我正在尝试 CodeDomProvider,然后是“provider.CompileAssemblyFromSource”
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-08
  • 2010-11-16
  • 1970-01-01
  • 1970-01-01
  • 2012-06-04
  • 1970-01-01
相关资源
最近更新 更多