折腾笔记[34]-csharp打包依赖dll到单个dll
摘要
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 enable
namespace 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
{
// 加载失败就算了...
}
}
}
}
}
}
// LoadResourceDll
namespace 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();
else
DiskLoader.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.cs
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
#pragma warning disable MSTEST0001
namespace 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()
{
// Arrange
const string testInput = "hello mstest";
// Act
string json = Generator.Generate(testInput);
// Assert
Assert.IsNotNull(json);
JObject obj = JObject.Parse(json); // 确保是合法 JSON
Assert.AreEqual(testInput, obj["Input"]?.Value<string>());
Assert.IsTrue(DateTime.TryParse(obj["Timestamp"]?.Value<string>(), out _));
}
[TestMethod]
public void Generate_NullInput_HandlesGracefully()
{
// Act
string json = Generator.Generate(null!);
// Assert
JObject 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()
{
// Act
string json = Generator.Generate(string.Empty);
// Assert
JObject 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. 生成 JSON
string 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"
}
按任意键退出...

浙公网安备 33010602011771号