在编译后的可执行文件中嵌入DLLs

719

可以将现有的DLL嵌入到编译后的C#可执行文件中吗(以便只需分发一个文件)?如果可能,如何进行操作?

通常,我会将DLL文件放在外部并让安装程序处理一切,但是工作中有几个人问过我这个问题,而我实在不知道该怎么做。


我建议您查看.NETZ实用程序,它还可以使用您选择的方案压缩汇编:http://madebits.com/netz/help.php#single - Nathan Baulch
除了ILMerge之外,如果您不想烦恼于命令行开关,我真的推荐ILMerge-Gui。这是一个开源项目,非常好用! - tyron
18个回答

838

我强烈建议使用Costura.Fody - 这是迄今为止嵌入资源到程序集中最好、最简单的方法。它可以作为NuGet包提供。

Install-Package Costura.Fody
在将其添加到项目后,它将自动将所有复制到输出目录的引用嵌入到您的程序集中。您可能希望通过向项目添加一个目标来清理嵌入的文件:
Install-CleanReferencesTarget

您还可以指定是否包括pdb文件,排除特定程序集或即时提取程序集。据我所知,也支持非托管程序集。

更新

目前,一些人正在尝试添加对DNX的支持(参考链接)。

更新2

对于最新的Fody版本,您需要使用MSBuild 16(即Visual Studio 2019)。Fody版本4.2.1将支持MSBuild 15。(参考:Fody is only supported on MSBuild 16 and above. Current version: 15


87
谢谢您提供这个绝妙的建议。安装这个软件包,就完成了。它甚至默认压缩程序集。 - Daniel
11
不想跟风,但我也是——这个工具让我省了不少麻烦!感谢你的推荐!现在我可以把需要重新分发的所有内容打包成一个单独的exe文件,而且它比原来的exe和dll文件加起来还要小。虽然我才用了几天,但除非出现什么问题,否则我觉得这会常常用到。 它就是好用! - mattezell
24
很酷,但有一个缺点:在Windows上生成的程序集与Linux下的Mono不再二进制兼容。这意味着你不能直接将该程序集部署到Linux的Mono上。 - Tyler Liu
8
太棒了!如果你正在使用vs2018,请不要忘记将FodyWeavers.xml文件放置在你的项目根目录下。 - Alan Deep
10
包管理器控制台命令Install-CleanReferencesTarget已不再有效,将会失败。在当前版本中已进行自动化处理。对于使用MSBuild 15的Visual Studio 2017,安装Nugets Fody 4.2.1Costura.Fody 3.3.3才能成功编译。 - Arvo Bowen
显示剩余22条评论

105

在Visual Studio中,右键单击您的项目,选择“项目属性”->“资源”->“添加资源”->“添加现有文件...”,然后将以下代码包含到您的App.xaml.cs或等效文件中。

public App()
{
    AppDomain.CurrentDomain.AssemblyResolve +=new ResolveEventHandler(CurrentDomain_AssemblyResolve);
}

System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    string dllName = args.Name.Contains(',') ? args.Name.Substring(0, args.Name.IndexOf(',')) : args.Name.Replace(".dll","");

    dllName = dllName.Replace(".", "_");

    if (dllName.EndsWith("_resources")) return null;

    System.Resources.ResourceManager rm = new System.Resources.ResourceManager(GetType().Namespace + ".Properties.Resources", System.Reflection.Assembly.GetExecutingAssembly());

    byte[] bytes = (byte[])rm.GetObject(dllName);

    return System.Reflection.Assembly.Load(bytes);
}

这是我原始的博客文章:http://codeblog.larsholm.net/2011/06/embed-dlls-easily-in-a-net-assembly/


6
您可以直接使用此功能。请查看我的答案https://dev59.com/qHVC5IYBdhLWcg3wxEJ1#20306095。 - Matthias
4
在您的博客上,有一条非常有用的评论来自AshRowe,请注意:如果您安装了自定义主题,它将尝试解析PresentationFramework.Theme程序集,这将导致崩溃!按照AshRowe的建议,您可以简单地检查dllName是否包含PresentationFramework,如下所示:如果(dllName.ToLower().Contains("presentationframework")) return null; - YasharBahman
5
对此有两点评论。第一:您应该检查bytes是否为null,如果是,则在那里返回null。毕竟,dll可能 不在 资源中。第二:只有当该类本身没有使用来自该程序集的任何内容时,这才起作用。对于命令行工具,我不得不将我的实际程序代码移动到一个新文件中,并创建一个小的新主程序,只需执行此操作,然后调用旧类中的原始主程序。 - Nyerguds
6
好的,你的答案是针对WPF的。我已经成功在Winforms中使用了。按照你所说的添加资源后,在Form构造函数中,在InitializeComponent()之前加入AppDomain.CurrentDomain.AssemblyResolve +=new ResolveEventHandler(CurrentDomain_AssemblyResolve);这一行代码。然后将整个System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)方法放在任何地方。编译并运行即可。这比最高得分的答案还要简单,因为不需要下载第三方工具。 - Dan W
5
如果有人遇到和我一样的问题:如果.dll文件名中包含连字符(比如twenty-two.dll),那么它们也会被替换成下划线(比如twenty_two.dll)。你可以将这行代码改为:dllName = dllName.Replace(".", "_").Replace("-", "_"); - Micah Vertal
显示剩余7条评论

90

我对本地DLL合并很感兴趣,有相关资料吗? - Baiyan Huang
5
请参见:https://dev59.com/43VD5IYBdhLWcg3wDG_m#156024 - Milan Gardian
@BaiyanHuang 请查看 https://github.com/boxedapp/bxilmerge,该项目的想法是为本地 DLL 制作类似于“ILMerge”的工具。 - Artem Razin
像我这样的 VB NET 开发者不要害怕链接中的 C++。ILMerge 对于 VB NET 也非常容易使用。在这里查看https://github.com/dotnet/ILMerge。感谢 @Shog9。 - Ivan Ferrer Villa

27

是的,可以将.NET可执行文件与库合并。有多个工具可用于完成此任务:

  • ILMerge 是一种实用程序,可用于将多个 .NET 程序集合并为单个程序集。
  • Mono mkbundle 将一个 exe 和所有带有 libmono 的程序集打包到一个二进制包中。
  • IL-Repack 是 ILMerge 的 FLOSS 替代品,并具有一些附加功能。

此外,这可以与 Mono Linker 结合使用,该工具可以删除未使用的代码,从而使生成的程序集更小。

另一个可能性是使用 .NETZ,它不仅允许压缩程序集,还可以直接将 dll 打包到 exe 中。与上述解决方案的区别在于 .NETZ 不会将它们合并,它们仍然是单独的程序集,但是被打包到一个包中。

.NETZ 是一种开源工具,用于压缩和打包 Microsoft .NET Framework 可执行文件(EXE、DLL),以使它们更小。


NETZ 似乎已经消失了。 - Robert Cutajar
哇 - 我以为我终于找到了它,然后我读了这个评论。看起来它已经完全消失了。有任何分支吗? - Mafii
好的,它只是转移到了GitHub上,不再与网站链接...所以“完全消失”有些言过其实。很可能它不再受支持,但它仍然存在。我更新了链接。 - Bobby

22

ILMerge可以将只包含托管代码的程序集合并为一个单独的程序集。您可以使用命令行应用程序,或者添加对exe的引用并以编程方式进行合并。对于GUI版本,有Eazfuscator.Netz,两者都是免费的。付费应用程序包括BoxedAppSmartAssembly

如果你需要合并带有非托管代码的程序集,我建议使用SmartAssembly。我从未在SmartAssembly上遇到过问题,但在其他工具上都有。这里,它可以将所需的依赖项作为资源嵌入到主exe中。
你可以手动完成所有这些操作,无需担心程序集是托管的还是混合模式的,只需将dll嵌入到资源中,然后依靠AppDomain的Assembly ResolveHandler。这是一个一站式解决方案,适用于最坏情况,即带有非托管代码的程序集。
static void Main()
{
    AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
    {
        string assemblyName = new AssemblyName(args.Name).Name;
        if (assemblyName.EndsWith(".resources"))
            return null;

        string dllName = assemblyName + ".dll";
        string dllFullPath = Path.Combine(GetMyApplicationSpecificPath(), dllName);

        using (Stream s = Assembly.GetEntryAssembly().GetManifestResourceStream(typeof(Program).Namespace + ".Resources." + dllName))
        {
            byte[] data = new byte[stream.Length];
            s.Read(data, 0, data.Length);

            //or just byte[] data = new BinaryReader(s).ReadBytes((int)s.Length);

            File.WriteAllBytes(dllFullPath, data);
        }

        return Assembly.LoadFrom(dllFullPath);
    };
}

关键在于将字节写入文件并从其位置加载。为避免鸡生蛋问题,您必须确保在访问程序集之前声明处理程序,并且在加载(程序集解析)部分内不访问程序集成员(或实例化任何与程序集有关的内容)。还要注意确保GetMyApplicationSpecificPath()不是任何临时目录,因为其他程序或您自己可能尝试删除临时文件(虽然它不会在您的程序访问dll时被删除,但至少会很烦人。AppData是一个好地方)。还要注意,您每次都必须写入字节,不能仅仅因为dll已驻留在那里而从位置加载。

对于托管dll,您无需编写字节,而是直接从dll的位置加载,或者只需读取字节并从内存加载程序集。就像这样:

    using (Stream s = Assembly.GetEntryAssembly().GetManifestResourceStream(typeof(Program).Namespace + ".Resources." + dllName))
    {
        byte[] data = new byte[stream.Length];
        s.Read(data, 0, data.Length);
        return Assembly.Load(data);
    }

    //or just

    return Assembly.LoadFrom(dllFullPath); //if location is known.

如果程序集完全是非托管的,您可以查看此linkthis以了解如何加载这些dll。

请注意,资源的“生成操作”需要设置为“嵌入式资源”。 - Mavamaarten
@Mavamaarten 不一定。如果提前将其添加到项目的 Resources.resx 中,您就不需要这样做。 - Nyerguds
2
EAZfuscator现在变成商业软件了。 - Telemat

18

.NET Core 3.0原生支持编译成一个单独的.exe文件

通过在项目文件(.csproj)中使用以下属性,可以启用此功能:

    <PropertyGroup>
        <PublishSingleFile>true</PublishSingleFile>
    </PropertyGroup>

这是在没有任何外部工具的情况下完成的。

有关更多详细信息,请参见我在这个问题的回答。


1
.NET 5支持这个吗? - Orace
3
你需要添加运行时标识符:win10-x64或linux-x64,例如:<RuntimeIdentifier>linux-x64</RuntimeIdentifier>。 - Nime Cloud

18

Jeffrey Richter的摘录很好。简而言之,将库作为嵌入资源添加,并在任何其他操作之前添加回调。以下是我在控制台应用程序的Main方法开头放置的代码版本(只需确保使用库的任何调用位于不同的方法中)。(该代码可在他的页面评论中找到。)

AppDomain.CurrentDomain.AssemblyResolve += (sender, bargs) =>
        {
            String dllName = new AssemblyName(bargs.Name).Name + ".dll";
            var assem = Assembly.GetExecutingAssembly();
            String resourceName = assem.GetManifestResourceNames().FirstOrDefault(rn => rn.EndsWith(dllName));
            if (resourceName == null) return null; // Not found, maybe another handler will find it
            using (var stream = assem.GetManifestResourceStream(resourceName))
            {
                Byte[] assemblyData = new Byte[stream.Length];
                stream.Read(assemblyData, 0, assemblyData.Length);
                return Assembly.Load(assemblyData);
            }
        };

1
稍微改了一下,完成了任务,谢谢伙计! - Sean Ed-Man
该项目 https://libz.codeplex.com/ 使用了这个过程,但它还会做一些其他的事情,比如为您管理事件处理程序和一些特殊代码,以避免破坏“托管可扩展框架目录”(如果仅使用此过程,则会破坏该目录)。 - Scott Chamberlain
太好了!!谢谢 @Steve - Ahmer Afzal

15

对上面@Bobby的回答进行扩展。您可以编辑.csproj文件,使用IL-Repack在构建时自动将所有文件打包为单个程序集。

  1. 使用Install-Package ILRepack.MSBuild.Task安装nuget包ILRepack.MSBuild.Task
  2. 编辑.csproj的AfterBuild部分

以下是一个简单的示例,将ExampleAssemblyToMerge.dll合并到项目输出中。

<!-- ILRepack -->
<Target Name="AfterBuild" Condition="'$(Configuration)' == 'Release'">

   <ItemGroup>
    <InputAssemblies Include="$(OutputPath)\$(AssemblyName).exe" />
    <InputAssemblies Include="$(OutputPath)\ExampleAssemblyToMerge.dll" />
   </ItemGroup>

   <ILRepack 
    Parallel="true"
    Internalize="true"
    InputAssemblies="@(InputAssemblies)"
    TargetKind="Exe"
    OutputFile="$(OutputPath)\$(AssemblyName).exe"
   />
</Target>

1
IL-Repack的语法已经改变,请查看链接的github仓库中的README.md文件(https://github.com/peters/ILRepack.MSBuild.Task)。这是唯一一个对我有效的方法,我能够使用通配符来匹配我想要包含的所有dll。 - Seabass77

12
以下方法不使用外部工具并且自动包含所有所需的DLL(无需手动操作,在编译时完成)。
我在这里看到很多答案都建议使用ILMergeILRepackJeffrey Ritcher方法,但这些方法都不能用于WPF应用程序,也不易于使用。
当你有大量DLL时,手动将需要的DLL包含在exe中可能会很困难。我发现最好的方法是由Wegged在StackOverflow上解释的。
为了清晰起见,在此复制他的答案(所有信用归Wegged所有)。

1)将以下内容添加到您的.csproj文件中:

<Target Name="AfterResolveReferences">
  <ItemGroup>
    <EmbeddedResource Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'">
      <LogicalName>%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName>
    </EmbeddedResource>
  </ItemGroup>
</Target>

2) 让你的主要Program.cs看起来像这样:

[STAThreadAttribute]
public static void Main()
{
    AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
    App.Main();
}

3) 添加 OnResolveAssembly 方法:

private static Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
{
    Assembly executingAssembly = Assembly.GetExecutingAssembly();
    AssemblyName assemblyName = new AssemblyName(args.Name);

    var path = assemblyName.Name + ".dll";
    if (assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture) == false) path = String.Format(@"{0}\{1}", assemblyName.CultureInfo, path);

    using (Stream stream = executingAssembly.GetManifestResourceStream(path))
    {
        if (stream == null) return null;

        var assemblyRawBytes = new byte[stream.Length];
        stream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length);
        return Assembly.Load(assemblyRawBytes);
    }
}

你能解释一下关于 CultureInfo 的测试吗?是否有 en-usfr-fr 子文件夹?这是指 DestinationSubDirectory 吗? - Orace
AfterResolveReferences和ReferenceCopyLocalPaths的值应该是什么?@Ludovic Feltz - Arjun Natarajan

9
您可以将DLL文件作为嵌入式资源添加,然后在启动时将其解压到应用程序目录中(在检查它们是否已经存在之后)。但是,制作安装程序非常容易,因此我认为这不值得。注意:对于.NET程序集而言,使用这种技术很容易。对于非.NET DLL文件而言,这将需要更多的工作(您需要找出在哪里解压文件并注册等)。

这里有一篇很棒的文章,解释了如何做到这一点:http://www.codeproject.com/Articles/528178/Load-DLL-From-Embedded-Resource - bluish

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接