如何在.NET Core控制台应用程序中加载位于文件夹中的程序集

46

我正在.NET Core平台上制作控制台应用程序,并想知道如何使用C#的动态特性加载程序集(.dll文件)并实例化类?它似乎与.NET 4.X有很大不同,而且没有真正记录...

例如,假设我有一个类库(.NET Core),它只有一个类:

namespace MyClassLib.SampleClasses
{
    public class Sample
    {
        public string SayHello(string name)
        {
            return $"Hello {name}";
        }

        public DateTime SayDateTime()
        {
            return DateTime.Now;
        }
    }
}

因此,dll文件的名称将是MyClassLib.dll,并且它位于/dlls/MyClassLib.dll中。

现在我想在一个简单的控制台应用程序 (.NET Core) 中加载它,使用C#的动态特性实例化 Sample 类并调用方法:

namespace AssemblyLoadingDynamic
{
    public class Program
    {
        public static void Main(string[] args)
        {
            // load the assembly and use the classes
        }
    }
}

注意: 我指的是 .NET Core 的 RC2 版本。

8个回答

45

当前针对 netcoreapp1.0 运行,你实际上不需要去实现自己的 AssemblyLoader。已经存在一个 Default,它可以正常工作。(因此 @VSG24 提到 Load 不起作用)。

using System;
using System.Runtime.Loader;

namespace AssemblyLoadingDynamic
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var myAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(@"C:\MyDirectory\bin\Custom.Thing.dll");
            var myType = myAssembly.GetType("Custom.Thing.SampleClass");
            var myInstance = Activator.CreateInstance(myType);
        }
    }   
}

有一个名为project.json的文件,看起来像这样:

{
  "version": "1.0.0-*",
  "buildOptions": {
    "emitEntryPoint": true
  },

  "dependencies": {
    "Microsoft.NETCore.App": {
      "type": "platform",
      "version": "1.0.1"
    },
    "System.Runtime.Loader": "4.0.0"
  },

  "frameworks": {
    "netcoreapp1.0": {
      "imports": "dnxcore50"
    }
  }
}

1
这比以前的答案好太多了。谢谢! - Blake
6
不加载 .NET Core 组件,令人惊讶地只加载普通的 .NET Framework 组件!!! - Saw

19

不确定这是否是最佳方法,但这是我想出来的:

(仅在 .Net Core RC2 - Windows 上进行过测试)

using System;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.DependencyModel;

namespace AssemblyLoadingDynamic
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var asl = new AssemblyLoader();
            var asm = asl.LoadFromAssemblyPath(@"C:\Location\Of\" + "SampleClassLib.dll");

            var type = asm.GetType("MyClassLib.SampleClasses.Sample");
            dynamic obj = Activator.CreateInstance(type);
            Console.WriteLine(obj.SayHello("John Doe"));
        }

        public class AssemblyLoader : AssemblyLoadContext
        {
            // Not exactly sure about this
            protected override Assembly Load(AssemblyName assemblyName)
            {
                var deps = DependencyContext.Default;
                var res = deps.CompileLibraries.Where(d => d.Name.Contains(assemblyName.Name)).ToList();
                var assembly = Assembly.Load(new AssemblyName(res.First().Name));
                return assembly;
            }
        }
    }
}

MyClassLib.SampleClasses是命名空间,Sample是类型或类名。

执行时,会尝试将编译好的SampleClassLib.dll类库加载到内存中,并使我的控制台应用程序可以访问MyClassLib.SampleClasses.Sample(请查看问题),然后我的应用程序调用SayHello()方法并将"John Doe"作为名称传递给它。因此程序打印:

"Hello John Doe"

快速提示: Load方法的重写无关紧要,因此可以将其内容替换为throw new NotImplementedException(),这不会影响我们关心的任何内容。


AssemblyLoadContext 在 asp.net core 1.0 中不可用,只能在 System.Runtime.Loader 4.0.0-beta-23516 中找到。 - Dilyan Dimitrov
@DilyanDimitrov 我在1.0版本上使用以上代码没有问题。 - Vahid Amiri
1
你可以在哪个包中找到 AssemblyLoadContext 类?我的 VS 建议添加我上面提到的包。 - Dilyan Dimitrov
@DilyanDimitrov 这是整个抽象类 https://gist.github.com/VSG24/9fe807a1f96073e2d4b2705a117e7439 正如我所说,如果你只想加载一些程序集,在 Load 方法中返回 null 即可。 - Vahid Amiri
这是一个老问题,但是这里有官方的MSDN链接来回答这个问题:“如何创建带插件支持的.NET Core应用程序” https://learn.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support - esertbas

11

感谢您的分享。这也适用于Net Core 1.0。如果您的程序集需要同一路径下的其他程序集,您可以使用下面的代码示例。

using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.DependencyModel;
public class AssemblyLoader : AssemblyLoadContext
{
    private string folderPath;

    public AssemblyLoader(string folderPath)
    {
        this.folderPath = folderPath;
    }

    protected override Assembly Load(AssemblyName assemblyName)
    {
        var deps = DependencyContext.Default;
        var res = deps.CompileLibraries.Where(d => d.Name.Contains(assemblyName.Name)).ToList();
        if (res.Count > 0)
        {
            return Assembly.Load(new AssemblyName(res.First().Name));
        }
        else
        {
            var apiApplicationFileInfo = new FileInfo($"{folderPath}{Path.DirectorySeparatorChar}{assemblyName.Name}.dll");
            if (File.Exists(apiApplicationFileInfo.FullName))
            {
                var asl = new AssemblyLoader(apiApplicationFileInfo.DirectoryName);
                return asl.LoadFromAssemblyPath(apiApplicationFileInfo.FullName);
            }
        }
        return Assembly.Load(assemblyName);
    }
}

请记得将以下依赖项添加到您的 project.json 文件中:

 "System.Runtime.Loader"
 "Microsoft.Extensions.DependencyModel"

对我来说非常有效 - Anthony
如果需要另一个程序集,它是否在.NET软件包目录中? - Joshua
这适用于加载程序集,但似乎不完全正确,检查新加载类型中基础类型的IsSubclassOf返回已知继承类型的false。 - MBulava

7
使用 .NET Core 1.1 / Standard 1.6 ,我发现 AssemblyLoader 不可用,而 AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath) 给出了一个 "无法加载文件或程序集 xxx" 的错误。

最终,以下解决方案对我起作用 - 只需添加一步获取 AssemblyName 对象:
var assemblyName = AssemblyLoadContext.GetAssemblyName(assemblyPath);
var assembly = Assembly.Load(assemblyName);

谢谢!不幸的是,我也有 AssemblyLoadContext.Default.Resolving 也会引发异常。 - cube45
第一个解决方案现在适用于.NET Core 2.x及更高版本。 - Rick Strahl

4

@Rob,我只能通过将“myInstance”类型更改为dynamic才能构建您的示例。

如果将类型保留为var,则可以构建代码,但是一旦尝试使用来自运行时加载的程序集的方法,就会出现编译器错误,例如myInstance不包含方法X。虽然我是新手,但标记类型为dynamic似乎是有道理的。如果类型在运行时加载,则编译器如何验证myInstance是否包含方法X或prop Y?通过将myInstance键入为dynamic,我相信您正在删除编译器检查,因此我可以很好地构建和运行示例。不确定这是否是100%正确的方式(我不了解足够多,您可能会建议使用dynamic存在问题?),但这是我无需费力创建自己的AssemblyLoader(正如您所指出的)而使其工作的唯一方法。

所以...

using System;
using System.Runtime.Loader;

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var myAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(@"C:\Documents\Visual Studio 2017\Projects\Foo\Foo\bin\Debug\netcoreapp2.0\Foo.dll");
            var myType = myAssembly.GetType("Foo.FooClass");
            dynamic myInstance = Activator.CreateInstance(myType);
            myInstance.UpperName("test");
        }
    }
}

希望这对新手有所帮助,因为我曾经花费很长时间才找到为什么我的实例作为一个 var 没有方法 X 等等的问题。


这是正确的。当处理动态类型时,类型推断通常无法工作。 - Vahid Amiri

1
我深入研究了这个问题,尝试了DependencyContext方法……它很有效,但有一些限制,并且与启动应用程序的C++ DotNet应用程序中的标准程序集解析不同。您必须手动进行名称匹配,如果您的宿主应用程序是已发布的,则您将无法获得NuGet文件夹的探测路径,这是一个问题(可解决),如果您的子程序集处于调试状态并使用NuGet...

所以这里有另一个解决方案:如果应用程序(AssemblyA)手动加载一个程序集(AssemblyB)没有依赖项(或与AssemblyB冲突的依赖项),我建议欺骗并默认为AssemblyB的程序集解析。Dotnet.exe有一个隐藏的宝石,可以让您加载所需的deps文件,因此您可以执行以下操作:

dotnet exec --depsfile pathToAssemblyB\assemblyB.deps.json --runtimeconfig pathToAssemblyB\assemblyB.runtimeconfig.json AssemblyA.dll

然后,您可以按照其他答案中的说明加载程序集,使用以下代码:

var myAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath("pathToAssemblyB\\AssemblyB.dll");

这样,它将正确解析所有与AssemblyB相关的依赖项,但不会解析与AssemblyA相关的依赖项。这是一个相反的问题,但如果您有一个想在大型应用程序中进行一些远程处理的小型应用程序,则非常有用。另一个问题是,您需要知道在启动应用程序时要使用AssemblyB,并且它仅在每次执行时起作用一次。因此,存在不同的问题集,您可以根据自己的情况选择适当的方法。请注意,这是一种不受支持/未记录的功能,但它在EF核心工具中使用,因此现在是“可行的”...


0
我一直在使用以下代码来加载程序集,并从已加载的程序集中调用类内的方法。
    private static FormCustomized loadLayout(global::System.String layoutFilename, global::System.String layoutNameSpace)
    {
        FormCustomized mainForm = default;
        Type typeMainLayout = default;
        FileInfo layoutFile;
        layoutFile = new FileInfo(layoutFilename);
        layoutFile.Refresh();
        if (!layoutFile.Exists)
        {
            MessageBox.Show("Layout file not found. You need to reinstall the program");
            return default;
        }

        try
        {
            Assembly assemblyRaw = Assembly.LoadFrom(layoutFilename);
            AssemblyLoadContext context = AssemblyLoadContext.Default;
            Assembly assembly = context.LoadFromAssemblyPath(layoutFilename);


            Type typeMainLayoutIni = assembly.GetType(layoutNameSpace + ".initializeLayoutClass");
            Object iniClass = Activator.CreateInstance(typeMainLayoutIni, true);
            MethodInfo methodInfo = typeMainLayoutIni.GetMethod("AssembliesToLoadAtStart");
            enVars.assemblies = (Dictionary<string, Environment.environmentAssembliesClass>)methodInfo.Invoke(iniClass, default);
            typeMainLayout = assembly.GetType(layoutNameSpace + ".mainAppLayoutForm");
            mainForm = Activator.CreateInstance(typeMainLayout, enVars) as FormCustomized;
        }
        catch (Exception ex)
        {
            return default;
        }

        return default;
    }

0

我认为以下内容适用于您,并希望这可以帮助像我一样的MEF2新手。

    /// <summary>
    /// Gets the assemblies that belong to the application .exe subfolder.
    /// </summary>
    /// <returns>A list of assemblies.</returns>
    private static IEnumerable<Assembly> GetAssemblies()
    {
        string executableLocation = AppContext.BaseDirectory;
        string directoryToSearch = Path.Combine(Path.GetDirectoryName(executableLocation), "Plugins");
        foreach (string file in Directory.EnumerateFiles(directoryToSearch, "*.dll"))
        {
            Assembly assembly = null;
            try
            {
                //Load assembly using byte array
                byte[] rawAssembly = File.ReadAllBytes(file);
                assembly = Assembly.Load(rawAssembly);
            }
            catch (Exception)
            {
            }

            if (assembly != null)
            {
                yield return assembly;
            }
        }
    }

另一个是在.netstandard1.3中,但两者都不可用。

var assembiles = Directory.GetFiles(Assembly.GetEntryAssembly().Location, "*.dll", SearchOption.TopDirectoryOnly)
        .Select(AssemblyLoadContext.Default.LoadFromAssemblyPath);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接