【问题标题】:How to properly load an instance from a DLL implementing a specific base class in C#?如何从在 C# 中实现特定基类的 DLL 中正确加载实例?
【发布时间】:2015-10-26 21:24:00
【问题描述】:

我有一个问题,我有一个程序应该从 DLL 实现特定基类的特定目录加载插件 (DLL)。问题是我加载 DLL 的程序引用了另一个 DLL,正在加载的 DLL 也引用了该 DLL。我将举例说明问题是如何产生的。这个简单的测试包括 3 个不同的解决方案和 3 个独立的项目。 注意:如果我将所有项目都放在同一个解决方案中,则不会出现问题。

解决方案 1 - 定义基类和接口的项目 AdapterBase.cs

namespace AdapterLib
{
    public interface IAdapter
    {
        void PrintHello();
    }

    public abstract class AdapterBase
    {
        protected abstract IAdapter Adapter { get; }

        public void PrintHello()
        {
            Adapter.PrintHello();
        }
    }
}

解决方案 2 - 定义基类实现的项目 MyAdapter.cs

namespace MyAdapter
{
    public class MyAdapter : AdapterBase
    {
        private IAdapter adapter;

        protected override IAdapter Adapter
        {
            get { return adapter ?? (adapter = new ImplementedTestClass()); }
        }
    }

    public class ImplementedTestClass : IAdapter
    {
        public void PrintHello()
        {
            Console.WriteLine("Hello beautiful worlds!");
        }
    }
}

解决方案 3 - 加载实现 AdapterBase* 的 DLL 的主程序 **Program.cs

namespace MyProgram {
    internal class Program {
        private static void Main(string[] args) {
            AdapterBase adapter = LoadAdapterFromPath("C:\\test\\Adapter");
            adapter.PrintHello();
        }

        public static AdapterBase LoadAdapterFromPath(string dir) {
            string[] files = Directory.GetFiles(dir, "*.dll");
            AdapterBase moduleToBeLoaded = null;
            foreach (var file in files) {
                Assembly assembly = Assembly.LoadFrom(file);
                foreach (Type type in assembly.GetTypes()) {
                    if (type.IsSubclassOf(typeof(AdapterBase))) {
                        try {
                            moduleToBeLoaded =
                                assembly.CreateInstance(type.FullName, false, BindingFlags.CreateInstance, null, null,
                                    null, null) as AdapterBase;
                        } catch (Exception ex) {

                        }
                        if (moduleToBeLoaded != null) {
                            return moduleToBeLoaded;
                        }
                    }
                }
            }
            return moduleToBeLoaded;
        }
    }
}

所以现在主程序 MyProgram.cs 将尝试从路径 C:\test\Adapter 加载 DLL,如果我 仅 将文件 MyAdapter.dll 放在该文件夹中。但是解决方案 2 (MyAdapter.cs) 会将 MyAdapter.dllAdapterBase.dll 都放在输出 bin/ 目录中。现在,如果将这两个文件都复制到 c:\test\Adapter,则 DLL 中的实例不会因为比较而被加载 if (type.IsSubclassOf(typeof(AdapterBase))) {MyProgram.cs 中失败。

由于 MyProgram.cs 已经引用了 AdapterBase.dll,因此从引用相同 DLL 的不同路径加载的其他 DLL 似乎存在一些冲突。加载的 DLL 似乎首先解决了它对同一文件夹中 DLL 的依赖关系。我认为这与程序集上下文和 LoadFrom 方法的一些问题有关,但我不知道如何让 C# 意识到它实际上是它已经加载的同一个 DLL。

一个解决方案当然只是复制唯一需要的 DLL,但如果我的程序能够处理其他共享 DLL 也在那里,它会更加健壮。我的意思是它们实际上是一样的。那么有什么解决方案可以让这更健壮吗?

【问题讨论】:

    标签: c# dll


    【解决方案1】:

    是的,这是一个类型标识问题。 .NET 类型的标识不仅仅是命名空间和类型名称,它还包括它来自的程序集。您的插件依赖于包含 IAdapter 的程序集,当 LoadFrom() 加载插件时,它也将需要该程序集。 CLR 在 LoadFrom 上下文中找到它,换句话说,在 c:\test\adapter 目录中,通常非常需要,因为它允许插件使用它们自己的 DLL 版本。

    只是在这种情况下。这出错了,因为您的插件解决方案尽职尽责地复制了依赖项。通常非常理想,只是在这种情况下不是。

    您必须阻止它复制 IAdapter 程序集:

    • 打开插件解决方案并使用 Build > Clean。
    • 使用 Explorer 删除输出目录中 IAdapter 程序集的剩余副本。
    • 在插件解决方案的 References 节点中选择 IAdapter 程序集。将其 Copy Local 属性设置为 False。
    • 使用构建 > 构建并验证 IAdapter 程序集确实不再被复制。

    Copy Local 是本质,其余的项目符号只是为了确保旧副本不会引起问题。由于 CLR 无法再以“简单的方式”找到 IAdapter 程序集,因此它不得不继续寻找它。现在在 Load 上下文中找到它,换句话说,就是安装主机可执行文件的目录。已经加载,无需再次加载唯一的。问题解决了。

    【讨论】:

    • 这是一个很好的答案,尽管我仍然对解决方案不太满意。不可能强制 LoadFrom 首先解析 MyPrograms 文件夹中的引用吗?或者只是告诉 LoadFrom 这个 DLL 实际上已经加载了,即 assembly1::namespace::Type 实际上与 assembly2::namespace::Type 相同?
    • 你要求更多的 DLL 地狱,我不能现实地给你那么长的绳子。唯一合理的方法是将程序集放在 GAC 中,这样总是安全可靠的。不是常见的 SO 建议,最好不要对简单的项目配置问题反应过度。
    • 好吧,我只能接受 Assembly.LoadFrom 无法做到这一点。但是,我发布了一个解决我的问题的解决方案,尽管该解决方案并不是问题真正要问的......
    • 嗯,Assembly.Load() 对于插件的扩展性相当差,您不能再将它们存储在单独的目录中,而必须将它们全部塞入一个目录中。现在,当一个插件覆盖另一个插件的 DLL 时,DLL Hell 就会发生,这很难诊断。这不是一个解决方案。
    【解决方案2】:

    我找到了解决问题的方法,尽管 DLL 的路径不能完全任意。例如,我能够将 DLL 放入 bin/MyCustomFolder 并加载 DLL,而不会出现类型冲突问题。

    解决方案是使用Assembly.Load() 方法,它将完整的程序集名称作为参数。所以首先我通过加载指定文件夹中的所有 DLL 并使用 Assembly.GetTypes() 并检查 Type 是否是 AdapterBase 的子类来找到程序集的名称。然后我使用Assembly.Load() 来实际加载程序集,它可以优雅地加载 DLL 而没有任何类型冲突。

    Program.cs

    namespace MyProgram {
        internal class Program {
            private static void Main(string[] args) {
    
                string codeBase = Assembly.GetExecutingAssembly().CodeBase;
                UriBuilder uri = new UriBuilder(codeBase);
                string path = Uri.UnescapeDataString(uri.Path);
                string dir = Path.GetDirectoryName(path);
                string pathToLoad = Path.Combine(dir, "MyCustomFolder");
                AdapterBase adapter = LoadAdapterFromPath(pathToLoad);
                adapter.PrintHello();
            }
    
            /// <summary>
            /// Loads the adapter from path. LoadFile will be used to find the correct type and then Assembly.Load will be used to actually load
            /// and instantiate the class.
            /// </summary>
            /// <param name="dir"></param>
            /// <returns></returns>
            public static AdapterBase LoadAdapterFromPath(string dir) {
                string assemblyName = FindAssembyNameForAdapterImplementation(dir);
                Assembly assembly = Assembly.Load(assemblyName);
                Type[] types = assembly.GetTypes();
                Type adapterType = null;
                foreach (var type in types)
                {
                    if (type.IsSubclassOf(typeof(AdapterBase)))
                    {
                        adapterType = type;
                        break;
                    }
                }
                AdapterBase adapter;
                try {
                    adapter = (AdapterBase)Activator.CreateInstance(adapterType);
                } catch (Exception e) {
                    adapter = null;
                }
                return adapter;
            }
    
            public static string FindAssembyNameForAdapterImplementation(string dir) {
                string[] files = Directory.GetFiles(dir, "*.dll");
                foreach (var file in files)
                {
                    Assembly assembly = Assembly.LoadFile(file);
                    foreach (Type type in assembly.GetTypes())
                    {
                        if (type.IsSubclassOf(typeof(AdapterBase)))
                        {
                            return assembly.FullName;
                        }
                    }
                }
                return null;
            }
        }
    }
    

    注意:Assembly.Load() 添加额外的探测路径以在bin/MyCustomFolder 中查找程序集也很重要。探测路径必须是执行程序集的子目录,因此不可能将 DLL 放在完全任意的位置。更新您的 App.config 如下:

    App.config

    <configuration>
      <runtime>
        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
          <probing privatePath="MyCustomFolder"/>
        </assemblyBinding>
      </runtime>
    </configuration>
    

    提示: 实际上,我在创建的 Web 应用程序中遇到了这个问题。在这种情况下,您应该更新 Web.config。同样在这种情况下,探测路径不会将执行程序集作为根,但实际上是 Web 应用程序的根。因此,在这种情况下,您可以将 DLL 文件夹 MyCustomFolder 直接放在 Web 应用程序根文件夹中,例如:inetpub\wwwroot\mywebapp\MyCustomFolder,然后将 Web.config 更新为上面的 App.config。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多