AppDomain.AssemblyResolve 內嵌 DLL 成單一 EXE 檔注意事項
0 |
開發小工具時,把相依的 DLL 包進單一 EXE 是很有用的技巧,如此使用者只需複製單一檔案到特定目錄或桌面便能執行,省去跑安裝程式或建立資料夾放入 EXE + DLL 的麻煩。
要達成這個理想,早期我是用 ILMerge 實現(參考:Visual Studio編譯小技巧:工具程式一檔搞定),但實務上遇到不少問題,例如:不支援 WPF、可能出現同名型別衝突、遇到某些 DLL 會失靈... 等等。後期我偏好另一種做法,編譯時自動內嵌參照 DLL + AppDomain.CurrentDomain.AssemblyResolve,遇到的問題少很多,但仍有些注意事項。
最容易犯的錯是 - 在 Program.cs Main() 執行之前就參考到外部 DLL。用以下這個引用 Newtonsoft.Json.dll 做 Json/XML 轉換的主控台程式當案例:
using Newtonsoft.Json;
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
namespace Json2Xml
{
class Program
{
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
try
{
string json = string.Empty;
if (Console.IsInputRedirected)
{
using (var sr = new StreamReader(Console.OpenStandardInput()))
{
json = sr.ReadToEnd();
}
}
else if (args.Any())
{
json = File.ReadAllText(args[0]);
}
else
{
Console.WriteLine("Syntax: Json2Xml json-file-name or use pipeline");
return;
}
var xml = JsonConvert.DeserializeXNode(json).ToString();
Console.WriteLine(xml);
}
catch (Exception ex)
{
Console.WriteLine($"ERROR - {ex.Message}");
}
}
private static Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
{
Assembly executingAssembly = Assembly.GetExecutingAssembly();
AssemblyName assemblyName = new AssemblyName(args.Name);
string 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;
byte[] assemblyRawBytes = new byte[stream.Length];
stream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length);
return Assembly.Load(assemblyRawBytes);
}
}
}
}
雖已成功將 Newtonsoft.Json.dll 併入 Json2Xml.exe,也宣告了 AppDomain.CurrentDomain.AssemblyResolve,但執行時仍會出現找不到 Json.NET 的錯誤訊息:
但只需稍做手腳,將 JsonConvert.DeserializeXNode() 移入獨立方法就可避開問題:
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
try
{
//... 略 ...
var xml = ConvJsonToXml(json);
Console.WriteLine(xml);
}
catch (Exception ex)
{
Console.WriteLine($"ERROR - {ex.Message}");
}
}
static string ConvJsonToXml(string json)
{
return JsonConvert.DeserializeXNode(json).ToString();
}
這樣就成功了! (這裡還順便練習了 Console Application 從 Pipeline 接資料的技巧)
另外還有一種狀況,如下例:
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
try
{
//... 略 ...
Console.WriteLine(JsonHelper.Parse(json).ToString());
}
catch (Exception ex)
{
Console.WriteLine($"ERROR - {ex.Message}");
}
}
程式看起來沒有引用 Json.NET,但一樣會發生找不到 未處理的例外狀況: System.IO.FileNotFoundException: 無法載入檔案或組件 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' 或其相依性的其中之一。 系統找不到指定的檔案。
錯誤,原因是 JsonHelper.Parse() 的回傳型別是 Json.NET 的 JObject 型別:
using Newtonsoft.Json.Linq;
namespace Json2Xml
{
public class JsonHelper
{
public static JObject Parse(string json)
{
return JObject.Parse(json);
}
}
}
依我的理解:由於要等 Main() 執行完,程式才具備解析所內嵌第三方程式庫的能力。若 Main() 本身涉及第三方程式庫,將導致 DLL 解析動作提前到 Main() 執行前的即時編譯過程,因而出錯。依此原理,上述程式只需稍微調整一下即可避開問題:
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
try
{
//... 略 ...
Console.WriteLine(ExractAsSomeFunc(json));
}
catch (Exception ex)
{
Console.WriteLine($"ERROR - {ex.Message}");
}
}
static string ExractAsSomeFunc(string json)
{
return JsonHelper.Parse(json).ToString();
}
過去沒搞清楚前,遇到都內嵌了還抱怨找不到 DLL 的狀況,我總是一頭霧水,歷經這番梳理,未來就知道怎麼做了。
Tips of how to use AppDomain.CurrentDomain.AssemblyResolve instead of ILMerge to embed DLLs into single EXE file.
Comments
Be the first to post a comment