8

AppDomain.AssemblyResolve 內嵌 DLL 成單一 EXE 檔注意事項

 3 years ago
source link: https://blog.darkthread.net/blog/netfx-single-file-exe/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
AppDomain.AssemblyResolve 內嵌 DLL 成單一 EXE 檔注意事項-黑暗執行緒

開發小工具時,把相依的 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 的狀況,我總是一頭霧水,歷經這番梳理,未來就知道怎麼做了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK