折腾笔记[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"
}

按任意键退出...
posted @ 2025-10-31 23:16  qsBye  阅读(5)  评论(0)    收藏  举报