摘要
csharp.net的库开发中打包依赖dll到最终输出的单个dll中.
实现
打包依赖dll为单文件
[https://github.com/gluck/il-repack]
[https://blog.walterlv.com/post/merge-assemblies-using-ilrepack.html]
[https://www.cnblogs.com/blqw/p/LoadResourceDll.html]
# 生成库文件
dotnet add package MSTest.TestAdapter  
dotnet add package MSTest.TestFramework
dotnet add package Newtonsoft.Json
# 复制dll文件到libs文件夹
dotnet build
# 复制生成的JusCore.dll到控制台程序工程目录# 控制台程序测试
dotnet new console -n Demo -f net8.0
cd Demo
dotnet build
dotnet run
1. 库:
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net8.0-windows</TargetFramework><Nullable>enable</Nullable><UseWPF>true</UseWPF><ImplicitUsings>enable</ImplicitUsings><AssemblyName>JusCore</AssemblyName></PropertyGroup><ItemGroup><PackageReference Include="ILRepack" Version="2.0.44"><PrivateAssets>all</PrivateAssets><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets></PackageReference><PackageReference Include="MSTest.TestAdapter" Version="4.0.1" PrivateAssets="all" /><PackageReference Include="MSTest.TestFramework" Version="4.0.1" PrivateAssets="all" /><PackageReference Include="Newtonsoft.Json" Version="13.0.4" PrivateAssets="all" /></ItemGroup><!-- 复制运行时依赖 --><PropertyGroup><CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies></PropertyGroup><!-- 2. 把 nuget 下载的 dll 拷到本地 libs 目录(仅第一次) --><Target Name="CollectRuntimeDlls" AfterTargets="Build" Condition="!Exists('libs\Newtonsoft.Json.dll')"><ItemGroup><_RuntimeDlls Include="$(PkgNewtonsoft_Json)\lib\net8.0\Newtonsoft.Json.dll" /></ItemGroup><Copy SourceFiles="@(_RuntimeDlls)" DestinationFolder="libs" SkipUnchangedFiles="true" /></Target><!-- 3. 把 libs 目录下所有 dll 设为嵌入资源 --><ItemGroup><EmbeddedResource Include="libs\*.dll" /></ItemGroup><!-- 4. 禁止它们再被复制到输出目录 --><Target Name="DisableCopyLocal" AfterTargets="ResolveAssemblyReferences"><ItemGroup><ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)" Condition="'%(Filename)'=='Newtonsoft.Json'" /></ItemGroup></Target></Project>
打包依赖为单个dll:
BundleDeps.cs
// 文件: BundleDeps.cs
// 功能: 打包所有依赖文件(EmbeddedResource)并合并到主文件using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.IO;
using System.Linq;#nullable enablenamespace JusCore.Tools
{/// <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){// 程序集Assembly ass;// 获取加载失败的程序集的全名var assName = new AssemblyName(args.Name).FullName;// 判断Dlls集合中是否有已加载的同名程序集if (Dlls.TryGetValue(assName!, out ass) && ass != null){// 如果有则置空并返回Dlls[assName] = null;return ass;}else{// 否则抛出加载失败的异常throw new DllNotFoundException(assName);}}/// <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);if(s == null) continue;var bts = new byte[s.Length];s.Read(bts, 0, (int)s.Length);var da = Assembly.Load(bts);// 判断是否已经加载if (Dlls.ContainsKey(da.FullName!)){continue;}Dlls[da.FullName!] = da;}catch{// 加载失败就算了...}}}}}
}// LoadResourceDllnamespace JusCore
{public static class MySelf{/// <summary>/// 唯一入口/// </summary>/// <param name="mode">/// "disk"   – 解压到磁盘再 LoadFrom(默认)  /// "memory" – 纯内存 Load(byte[]),无临时文件/// </param>public static void Init(string? mode = "disk"){if (mode is "memory")MemoryLoader.Load();elseDiskLoader.Load();}}#region disk 模式internal static class DiskLoader{private static bool _done;public static void Load(){if (_done) return;_done = true;var myself = Assembly.GetExecutingAssembly();var location = myself.Location;if (string.IsNullOrEmpty(location)) return;   // 单文件发布时放弃var targetDir = Path.Combine(Path.GetDirectoryName(location)!, "JusCore");Directory.CreateDirectory(targetDir);foreach (var resName in myself.GetManifestResourceNames().Where(n => n.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)).OrderBy(n => n))   // 保证顺序{var fileName = Path.GetFileName(resName);var targetPath = Path.Combine(targetDir, fileName);using var resStream = myself.GetManifestResourceStream(resName);if (resStream is null) continue;if (File.Exists(targetPath) && new FileInfo(targetPath).Length == resStream.Length)continue;resStream.Position = 0;using var fs = File.Create(targetPath);resStream.CopyTo(fs);// 文件句柄问题// _ = Assembly.LoadFrom(targetPath);byte[] bytes = File.ReadAllBytes(targetPath);Assembly.Load(bytes);}}}#endregion#region memory 模式internal static class MemoryLoader{private static bool _done;private static readonly Dictionary<string, Assembly> _loaded = new();public static void Load(){if (_done) return;_done = true;var myself = Assembly.GetExecutingAssembly();// 1. 先全部读进内存foreach (var resName in myself.GetManifestResourceNames().Where(n => n.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)).OrderBy(n => n)){using var stream = myself.GetManifestResourceStream(resName);if (stream is null) continue;var bytes = new byte[stream.Length];_ = stream.Read(bytes, 0, bytes.Length);var asm = Assembly.Load(bytes);_loaded[asm.FullName!] = asm;}// 2. 注册兜底回调AppDomain.CurrentDomain.AssemblyResolve += OnResolve;}private static Assembly? OnResolve(object? sender, ResolveEventArgs args){var name = new AssemblyName(args.Name).FullName;return _loaded.TryGetValue(name, out var asm) ? asm : null;}}#endregion
}
库的功能实现:
Generator.cs
// 文件: Generator.csusing System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;#pragma warning disable MSTEST0001namespace JusCore;public static class Generator
{/// <summary>/// 唯一公开方法:输入任意字符串,输出 JSON 字符串/// 这里仅做简单包装,把输入当成一个字段值序列化。/// 你可以按需要把 input 解析成别的对象再序列化。/// </summary>public static string Generate(string input){var dto = new { Input = input, Timestamp = DateTime.UtcNow };return JsonConvert.SerializeObject(dto, Formatting.Indented);}
}[TestClass]
public class GeneratorTests
{[TestMethod]public void Generate_ValidInput_ReturnsValidJsonWithInputAndTimestamp(){// Arrangeconst string testInput = "hello mstest";// Actstring json = Generator.Generate(testInput);// AssertAssert.IsNotNull(json);JObject obj = JObject.Parse(json);           // 确保是合法 JSONAssert.AreEqual(testInput, obj["Input"]?.Value<string>());Assert.IsTrue(DateTime.TryParse(obj["Timestamp"]?.Value<string>(), out _));}[TestMethod]public void Generate_NullInput_HandlesGracefully(){// Actstring json = Generator.Generate(null!);// AssertJObject obj = JObject.Parse(json);Assert.IsNull(obj["Input"]?.Value<string>());Assert.IsTrue(DateTime.TryParse(obj["Timestamp"]?.Value<string>(), out _));}[TestMethod]public void Generate_EmptyInput_ReturnsJsonWithEmptyInput(){// Actstring json = Generator.Generate(string.Empty);// AssertJObject obj = JObject.Parse(json);Assert.AreEqual(string.Empty, obj["Input"]?.Value<string>());}
}
复制依赖dll文件到libs文件夹;
复制输出的JusCore.dll文件到控制台程序工程目录;
2. 控制台程序引用库文件:
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup><ItemGroup><Reference Include="JusCore"><HintPath>JusCore.dll</HintPath></Reference></ItemGroup></Project>
测试程序:
Program.cs
using System;
using JusCore;   // Generator 与 MySelf 都在此命名空间namespace Demo
{internal class Program{static void Main(){// 1. 初始化依赖(disk 模式有bug)// MySelf.Init("disk");MySelf.Init("memory");// 2. 生成 JSONstring json = Generator.Generate("hello");// 3. 打印Console.WriteLine(json);// 4. 防止控制台一闪而过Console.WriteLine("\n按任意键退出...");Console.ReadKey();}}
}
预期输出:
在 3.0 秒内生成 成功,出现 2 警告
{"Input": "hello","Timestamp": "2025-10-31T15:05:50.83119Z"
}按任意键退出...