開發小工具時,把相依的 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

Post a comment