如何在.NET中将类库插件发布为单个文件?
插件架构下的单文件打包与加载方案
从归档文件加载程序集的实现
你可以在CI/CD流水线中将每个插件的发布目录压缩为单独的ZIP归档,然后在主应用中直接从归档加载程序集,无需解压到磁盘。以下是具体实现步骤和代码示例:
1. CI/CD流水线打包插件(以GitHub Actions为例)
在流水线中完成插件的发布与压缩,输出单个ZIP文件:
# .github/workflows/publish-plugins.yml name: Publish Plugins on: [push] jobs: build-plugin1: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - name: Publish Plugin1 run: dotnet publish ./src/Plugin1/Plugin1.csproj -c Release -o ./publish/Plugin1 - name: Zip Plugin1 run: zip -r Plugin1.zip ./publish/Plugin1 - name: Upload Plugin1 Zip uses: actions/upload-artifact@v4 with: name: Plugin1 path: Plugin1.zip
2. 主应用从ZIP加载插件的代码示例
使用AssemblyLoadContext隔离插件加载环境,避免与主应用依赖冲突,直接从ZIP流中读取程序集:
首先定义自定义加载上下文:
using System.IO.Compression; using System.Reflection; using System.Runtime.Loader; public class PluginAssemblyLoadContext : AssemblyLoadContext { private readonly Dictionary<string, Assembly> _loadedAssemblies = new(); private readonly ZipArchive _zipArchive; public PluginAssemblyLoadContext(ZipArchive zipArchive) : base(isCollectible: true) { _zipArchive = zipArchive; } protected override Assembly? Load(AssemblyName assemblyName) { var assemblyFileName = $"{assemblyName.Name}.dll"; if (_loadedAssemblies.TryGetValue(assemblyFileName, out var existing)) return existing; // 从ZIP中查找目标程序集 var entry = _zipArchive.GetEntry(assemblyFileName); if (entry == null) return null; // 系统依赖交给默认上下文加载 // 读取程序集字节到内存并加载 using var stream = entry.Open(); var assemblyBytes = new byte[stream.Length]; stream.Read(assemblyBytes, 0, assemblyBytes.Length); var assembly = LoadFromStream(new MemoryStream(assemblyBytes)); _loadedAssemblies[assemblyFileName] = assembly; return assembly; } }
然后修改主应用的插件加载逻辑:
public void LoadPluginsFromZip(string zipFilePath) { using var zipArchive = ZipFile.OpenRead(zipFilePath); var loadContext = new PluginAssemblyLoadContext(zipArchive); // 遍历ZIP中的所有DLL文件 foreach (var entry in zipArchive.Entries.Where(e => e.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))) { try { var assemblyName = AssemblyName.GetAssemblyName(entry.FullName); var assembly = loadContext.LoadFromAssemblyName(assemblyName); // 查找并实例化IPlugin实现类 foreach (var type in assembly.GetTypes()) { if (typeof(IPlugin).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract) { var plugin = (IPlugin)Activator.CreateInstance(type)!; plugin.Start(this); // this为IApp的实现实例 } } } catch (Exception ex) { Console.WriteLine($"加载插件 {entry.Name} 失败: {ex.Message}"); } } }
更优替代方案
1. 使用ILRepack合并插件到单个DLL
ILRepack是开源的程序集合并工具,可将插件及其所有依赖合并为单个DLL,无需压缩包。
- 安装ILRepack NuGet包到插件项目:
dotnet add package ILRepack - 在插件项目文件中添加合并配置:
<PropertyGroup> <TargetFramework>net8.0</TargetFramework> <ILRepack>True</ILRepack> <ILRepackOutput>$(AssemblyName).dll</ILRepackOutput> <ILRepackMergeAllTrue>True</ILRepackMergeAllTrue> </PropertyGroup>
发布后会生成单个包含所有依赖的DLL,主应用直接扫描加载该文件即可。
2. 自定义AssemblyLoadContext加载合并后的DLL
为避免插件与主应用的依赖冲突,使用独立的加载上下文加载合并后的DLL:
public class IsolatedPluginLoadContext : AssemblyLoadContext { protected override Assembly? Load(AssemblyName assemblyName) { // 插件内部依赖优先加载,系统依赖交给默认上下文处理 return null; } } // 加载合并后的插件DLL var loadContext = new IsolatedPluginLoadContext(); var assembly = loadContext.LoadFromAssemblyPath("Plugin1.dll"); // 扫描并实例化插件(逻辑同之前的类型查找) foreach (var type in assembly.GetTypes()) { if (typeof(IPlugin).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract) { var plugin = (IPlugin)Activator.CreateInstance(type)!; plugin.Start(this); } }
3. 打包为NuGet包部署
将每个插件打包为NuGet包,发布到私有NuGet源,主应用启动时从源下载并提取程序集(本质也是从ZIP加载,NuGet包为ZIP格式),适合团队协作和版本管理。
注意事项
- 从内存加载程序集时,若插件存在非托管依赖,需单独处理(如将非托管DLL放在指定目录,通过
NativeLibrary.Load加载)。 - 合并程序集时,若插件有强签名,需保留签名或重新生成签名。
- 使用
AssemblyLoadContext的isCollectible: true参数可在卸载插件时回收资源,适合动态插件场景。
内容的提问来源于stack exchange,提问作者Sly Redbeard
相关产品推荐
相关产品推荐

