C# 将 DLL 打包嵌入资源并实现自动加载详解
在 C# 开发中,将外部 DLL 嵌入程序资源是解决运行时依赖缺失、实现单文件发布的常用技术。通过监听 AppDomain.CurrentDomain.AssemblyResolve 事件,可以在系统找不到对应程序集时,从嵌入的资源流中动态加载 DLL,从而避免分发多个文件。
默认加载行为与依赖问题
假设项目中引用了一个名为 TestLibrary1.dll 的第三方库,其中包含一个测试类:
namespace TestLibrary1
{
public class Test
{
public void Point()
{
Console.WriteLine("aaabbbccc");
}
}
}
在主程序中调用该方法:
using System;
namespace ConsoleApplication5
{
class Program
{
static void Main(string[] args)
{
var test = new TestLibrary1.Test();
test.Point();
Console.ReadLine();
}
}
}
当生成的 .exe 与 TestLibrary1.dll 位于同一目录时,程序正常运行。但如果只发送 .exe 文件而未附带 DLL,运行时便会因找不到依赖而崩溃:
解决方案:资源嵌入与事件劫持
C# 提供了 AppDomain.CurrentDomain.AssemblyResolve 事件,用于捕获程序集加载失败的情况。我们可以将 DLL 添加为项目的“嵌入式资源”,并在该事件中从资源流读取字节并加载。
具体操作步骤如下:
第1步:将 DLL 添加为嵌入式资源
在 Visual Studio 项目中右键添加现有文件(如 TestLibrary1.dll),并将文件的“生成操作”属性设置为“嵌入的资源”。这确保了 DLL 会被打包进最终的 .exe 文件中。
第2步:绑定 AssemblyResolve 事件
需要在任何使用到该程序集的代码执行之前绑定事件,通常在静态构造函数中完成:
class Program
{
static Program()
{
// 必须在使用 TestLibrary1 之前绑定事件,否则首次加载时会直接抛出异常
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
}
static System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
// 获取失败的程序集全名
var assName = new AssemblyName(args.Name).FullName;
// 匹配目标程序集名称(可根据需要修改判断逻辑)
if (args.Name == "TestLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")
{
// 从嵌入资源中读取 DLL 字节
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ConsoleApplication5.TestLibrary1.dll"))
{
var bytes = new byte[stream.Length];
stream.Read(bytes, 0, (int)stream.Length);
// 从字节数组加载程序集
return Assembly.Load(bytes);
}
}
// 如果没找到匹配的 DLL,抛出原始异常
throw new DllNotFoundException(assName);
}
static void Main(string[] args)
{
var test = new TestLibrary1.Test();
test.Point();
Console.ReadLine();
}
}
配置完成后,程序即可独立运行,无需携带额外的 DLL 文件:
通用方案:批量加载多个资源 DLL
若项目中包含大量 DLL,逐个判断名字不仅繁琐且难以维护。可以通过反射遍历所有嵌入资源,自动加载所有后缀为 .dll 的文件,并利用字典缓存已加载的程序集以提高性能。
第1步:创建通用加载类
以下 LoadResourceDll 类可复用。它通过 StackTrace 获取调用者的程序集,扫描其中的嵌入式资源,并将所有 .dll 预加载到内存字典中。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
namespace blqw
{
/// <summary> 载入资源中的动态链接库(dll)文件
/// </summary>
static class LoadResourceDll
{
static Dictionary<string, Assembly> Dlls = new Dictionary<string, Assembly>();
static Dictionary<string, object> Assemblies = new Dictionary<string, object>();
static Assembly AssemblyResolve(object sender, ResolveEventArgs args)
{
// 检查是否已在缓存中
if (Dlls.TryGetValue(new AssemblyName(args.Name).FullName, out Assembly ass) && ass != null)
{
Dlls[new AssemblyName(args.Name).FullName] = null; // 已加载过,置空防止重复查找
return ass;
}
else
{
throw new DllNotFoundException(args.Name);
}
}
/// <summary> 注册资源中的dll
/// </summary>
public static void RegistDLL()
{
// 获取调用者的程序集
var ass = new StackTrace(0).GetFrame(1).GetMethod().Module.Assembly;
// 防止重复注册
if (Assemblies.ContainsKey(ass.FullName))
{
return;
}
Assemblies.Add(ass.FullName, null);
// 绑定全局解析事件
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
// 遍历所有嵌入资源名称
var res = ass.GetManifestResourceNames();
foreach (var r in res)
{
// 仅处理 .dll 文件
if (r.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
{
try
{
using (var s = ass.GetManifestResourceStream(r))
{
var bts = new byte[s.Length];
s.Read(bts, 0, (int)s.Length);
var da = Assembly.Load(bts);
// 存入缓存字典,键为程序集全名
if (!Dlls.ContainsKey(da.FullName))
{
Dlls[da.FullName] = da;
}
}
}
catch
{
// 忽略无法加载的资源(如损坏的文件)
}
}
}
}
}
}
第2步:在项目启动处调用注册方法
只需在任何逻辑执行前调用一次 RegistDLL(),项目中的所有嵌入式 DLL 都会被自动检测并注册。当系统报出任何 DLL 缺失时,事件处理程序会先从字典中查找是否存在同名资源,存在则直接返回,从而实现透明加载。
完成判断
编译运行后,确认主程序能正常实例化并调用不同 DLL 中的类型,且输出目录下无额外产生的 DLL 文件,即表示资源加载配置成功:
以上就是将 C# DLL 打包嵌入资源并实现自动加载的详细内容,更多关于 C# 程序集管理与单文件发布优化的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













