是否可以将预先存在的 DLL 嵌入到已编译的 C# 可执行文件中(这样您只有一个要分发的文件)?如果有可能,人们将如何去做呢?
通常,我很乐意将 DLL 留在外面并让安装程序处理所有事情,但是有几个工作人员问过我这个问题,我真的不知道。
我强烈推荐使用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 仅支持 MSBuild 16 及更高版本。当前版本:15)
只需在 Visual Studio 中右键单击您的项目,选择 Project Properties -> Resources -> Add Resource -> Add Existing File... 并将以下代码包含到您的 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/
如果它们实际上是托管程序集,则可以使用ILMerge。对于本机 DLL,您需要做更多的工作。
是的,可以将 .NET 可执行文件与库合并。有多种工具可用于完成工作:
此外,这可以与Mono Linker结合使用,它确实删除了未使用的代码,因此使生成的程序集更小。
另一种可能性是使用.NETZ,它不仅允许压缩程序集,还可以将 dll 直接打包到 exe 中。与上述解决方案的不同之处在于 .NETZ 不会合并它们,它们保持独立的程序集,但被打包到一个包中。
.NETZ 是一个开源工具,它压缩和打包 Microsoft .NET Framework 可执行文件(EXE、DLL)以使它们更小。
如果程序集只有托管代码, ILMerge可以将程序集组合成一个程序集。您可以使用命令行应用程序,或添加对 exe 的引用并以编程方式合并。对于 GUI 版本,有Eazfuscator和.Netz ,两者都是免费的。付费应用程序包括BoxedApp和SmartAssembly。
如果您必须将程序集与非托管代码合并,我建议您使用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.
Jeffrey Richter的摘录非常好。简而言之,将库添加为嵌入式资源并在其他任何内容之前添加回调。这是我在控制台应用程序的 Main 方法开头放置的代码版本(在他的页面的评论中找到)(只需确保使用该库的任何调用与 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);
}
};
扩展上面的@Bobby 的回答。您可以编辑 .csproj 以在构建时使用IL-Repack自动将所有文件打包到单个程序集中。
Install-Package ILRepack.MSBuild.Task
这是一个简单的示例,它将 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>
该功能通过在项目文件 (.csproj) 中使用以下属性来启用:
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>
这是在没有任何外部工具的情况下完成的。
有关更多详细信息,请参阅我对这个问题的回答。
您可以将 DLL 作为嵌入式资源添加,然后让您的程序在启动时将它们解压缩到应用程序目录中(在检查它们是否已经存在之后)。
但是,设置文件很容易制作,我认为这不值得。
编辑:这种技术对于 .NET 程序集很容易。使用非 .NET DLL 会做更多的工作(您必须弄清楚在哪里解压文件并注册它们等等)。
另一个可以优雅地处理这个问题的产品是 SmartAssembly,位于SmartAssembly.com。除了将所有依赖项合并到一个 DLL 之外,该产品还将(可选地)混淆您的代码,删除额外的元数据以减小生成的文件大小,并且实际上还可以优化 IL 以提高运行时性能。
它还向您的软件(如果需要)添加了某种可能有用的全局异常处理/报告功能。我相信它还有一个命令行 API,因此您可以将其作为构建过程的一部分。
ILMerge 方法和 Lars Holm Jensen 处理 AssemblyResolve 事件都不适用于插件主机。假设可执行程序H动态加载程序集P并通过在单独程序集中定义的接口IP访问它。要将IP嵌入到H中,需要对 Lars 的代码稍作修改:
Dictionary<string, Assembly> loaded = new Dictionary<string,Assembly>();
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{ Assembly resAssembly;
string dllName = args.Name.Contains(",") ? args.Name.Substring(0, args.Name.IndexOf(',')) : args.Name.Replace(".dll","");
dllName = dllName.Replace(".", "_");
if ( !loaded.ContainsKey( dllName ) )
{ 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);
resAssembly = System.Reflection.Assembly.Load(bytes);
loaded.Add(dllName, resAssembly);
}
else
{ resAssembly = loaded[dllName]; }
return resAssembly;
};
处理重复尝试解析相同程序集并返回现有程序集而不是创建新实例的技巧。
编辑: 以免破坏 .NET 的序列化,确保为所有未嵌入您的程序集返回 null,从而默认为标准行为。您可以通过以下方式获取这些库的列表:
static HashSet<string> IncludedAssemblies = new HashSet<string>();
string[] resources = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceNames();
for(int i = 0; i < resources.Length; i++)
{ IncludedAssemblies.Add(resources[i]); }
如果传递的程序集不属于,则返回 null IncludedAssemblies
。
以下方法不要使用外部工具,并自动包含所有需要的 DLL(无需手动操作,一切都在编译时完成)
我在这里阅读了很多答案,说要使用ILMerge、ILRepack或Jeffrey Ritcher方法,但这些方法都不适用于WPF 应用程序,也不容易使用。
当您有很多 DLL 时,可能很难在 exe 中手动包含您需要的那个。Wegged在 StackOverflow 上解释了我发现的最佳方法
为了清楚起见,复制粘贴了他的答案(所有功劳归于 Wegged)
.csproj
文件中:<Target Name="AfterResolveReferences">
<ItemGroup>
<EmbeddedResource Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'">
<LogicalName>%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
</Target>
Program.cs
看起来像这样:[STAThreadAttribute]
public static void Main()
{
AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
App.Main();
}
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);
}
}
这听起来可能很简单,但 WinRar 提供了将一堆文件压缩为自解压可执行文件的选项。
它有许多可配置的选项:最终图标、提取文件到给定路径、提取后要执行的文件、提取期间显示的自定义徽标/弹出窗口文本、根本没有弹出窗口、许可协议文本等。
在某些情况下可能很有用.
我使用从 .vbs 脚本调用的 csc.exe 编译器。
在您的 xyz.cs 脚本中,在指令之后添加以下行(我的示例是 Renci SSH):
using System;
using Renci;//FOR THE SSH
using System.Net;//FOR THE ADDRESS TRANSLATION
using System.Reflection;//FOR THE Assembly
//+ref>"C:\Program Files (x86)\Microsoft\ILMerge\Renci.SshNet.dll"
//+res>"C:\Program Files (x86)\Microsoft\ILMerge\Renci.SshNet.dll"
//+ico>"C:\Program Files (x86)\Microsoft CAPICOM 2.1.0.2 SDK\Samples\c_sharp\xmldsig\resources\Traffic.ico"
ref、res 和 ico 标签将被下面的 .vbs 脚本拾取以形成 csc 命令。
然后在 Main 中添加程序集解析器调用程序:
public static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
.
...并将解析器本身添加到类中的某个位置:
静态程序集 CurrentDomain_AssemblyResolve(对象发送者,ResolveEventArgs args) { String resourceName = new AssemblyName(args.Name).Name + ".dll"; 使用 (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) { 字节[] 组装数据 = 新字节[流.长度]; 流.Read(assemblyData, 0, assemblyData.Length); 返回 Assembly.Load(assemblyData); } }
我命名 vbs 脚本以匹配 .cs 文件名(例如 ssh.vbs 查找 ssh.cs);这使得多次运行脚本变得容易得多,但如果你不是像我这样的白痴,那么通用脚本可以从拖放中获取目标 .cs 文件:
暗淡名称_,oShell,fso 设置 oShell = CreateObject("Shell.Application") 设置 fso = CreateObject("Scripting.fileSystemObject") '将 VBS 脚本名作为目标文件名 '############################################### name_ = Split(wscript.ScriptName, ".")(0) '从 .CS 文件中获取外部 DLL 和图标名称 '################################################ ###### 常量 OPEN_FILE_FOR_READING = 1 设置 objInputFile = fso.OpenTextFile(name_ & ".cs", 1) '将所有内容读入数组 '############################# inputData = Split(objInputFile.ReadAll, vbNewline) 对于每个 strData 中的 inputData 如果 left(strData,7)="//+ref>" 那么 csc_references = csc_references & " /reference:" & trim(replace(strData,"//+ref>","")) & " " 万一 如果 left(strData,7)="//+res>" 那么 csc_resources = csc_resources & " /resource:" & trim(replace(strData,"//+res>","")) & " " 万一 如果 left(strData,7)="//+ico>" 那么 csc_icon = " /win32icon:" & trim(replace(strData,"//+ico>","")) & " " 万一 下一个 objInputFile.Close '编译文件 '################ oShell.ShellExecute "c:\windows\microsoft.net\framework\v3.5\csc.exe", "/warn:1 /target:exe " & csc_references & csc_resources & csc_icon & " " & name_ & ".cs" , "", "runas", 2 WScript.Quit(0)
在 C# 中创建混合本机/托管程序集是可能的,但并非那么容易。如果您使用 C++,它会容易得多,因为 Visual C++ 编译器可以像其他任何东西一样轻松地创建混合程序集。
除非您对生成混合程序集有严格的要求,否则我同意 MusiGenesis 的观点,即这不值得用 C# 来做这件事。如果您需要这样做,或许可以考虑改用 C++/CLI。
通常,您需要某种形式的后期构建工具来执行您所描述的程序集合并。有一个名为 Eazfuscator (eazfuscator.blogspot.com/) 的免费工具,它专为字节码处理而设计,也可以处理程序集合并。您可以使用 Visual Studio 将其添加到构建后的命令行中以合并您的程序集,但是由于在任何非平凡的程序集合并方案中会出现问题,您的工作量会有所不同。
您还可以检查 build make untility NANT 是否能够在构建后合并程序集,但我自己对 NANT 还不够熟悉,无法说出该功能是否内置。
还有许多 Visual Studio 插件将执行程序集合并作为构建应用程序的一部分。
或者,如果您不需要自动完成此操作,有许多工具(如 ILMerge)会将 .net 程序集合并到单个文件中。
我在合并程序集时遇到的最大问题是它们是否使用任何类似的命名空间。或者更糟糕的是,引用同一个 dll 的不同版本(我的问题通常与 NUnit dll 文件有关)。