被 SQLite + .NET Framework 問題困住,纏鬥一陣子才弄清楚狀況,也找到了解法,特筆記備忘。

問題發生在 .NET Framework 專案引用 Microsoft.Data.Sqlite 6.0 冒出DllNotFoundException: Unable to load DLL 'e_sqlite3'執行階段錯誤,.NET Core/.NET 6 使用該版本則一切正常。實測 .NET Framework 使用 Microsoft.Data.Sqlite v3.0 以上版本都會出錯,降版到 v2.2 才沒問題:

對照 Microsoft.Data.Sqlite v2.2 跟 v3.0 的主要差異是,v2.2 引用 SQLitePCLRaw.bundle_green 1.1.x,v3.0 起改用 SQLitePCLRaw.bundle_e_sqlite3 2.0 以上的版本。原本正常的 Microsoft.Data.Sqlite v2.2,若將 SQLitePCLRaw.bundle_green 升級到 v2.0+ 也會出錯,由此判定 SQLitePCLRaw 新版有問題。延伸閱讀:SQLite 原生程式庫 - SQLitePCLRaw 版本說明

爬文查到是 Microsoft.Data.Sqlite 6.0.9 預設安裝的 SQLitePCLRaw.bundle_e_sqlite 2.0.6 版有問題,在 v2.0.7 版時,針對 .NET Framework 做了修正,實測在 Console Application 專案將 SQLitePCLRaw.bundle_e_sqlite3 升級到 v2.0.7 可排除問題,但如果是 ASP.NET 專案,升級到 v2.0.7 仍會繼續噴錯,但錯誤訊息有點不同:

例外狀況詳細資訊: System.Exception: Library e_sqlite3 not found
plat: win
suffix: DLL
possibilities (2):
   1) C:\Users\jeffrey\AppData\Local\Temp\Temporary ASP.NET Files\vs\090ba58f\317721b4\assembly\dl3\c8a87db4\004c6898_94cbd801\runtimes\win-x64\native\e_sqlite3.dll
   2) C:\Users\jeffrey\AppData\Local\Temp\Temporary ASP.NET Files\vs\090ba58f\317721b4\assembly\dl3\c8a87db4\004c6898_94cbd801\e_sqlite3.dll
win TryLoad: C:\Users\jeffrey\AppData\Local\Temp\Temporary ASP.NET Files\vs\090ba58f\317721b4\assembly\dl3\c8a87db4\004c6898_94cbd801\runtimes\win-x64\native\e_sqlite3.dll
thrown: System.ComponentModel.Win32Exception (0x80004005): 找不到指定的模組。
  於 SQLitePCL.NativeLibrary.TryLoad(String name, Loader plat, Action`1 log, IntPtr& h)
win TryLoad: C:\Users\jeffrey\AppData\Local\Temp\Temporary ASP.NET Files\vs\090ba58f\317721b4\assembly\dl3\c8a87db4\004c6898_94cbd801\e_sqlite3.dll
thrown: System.ComponentModel.Win32Exception (0x80004005): 找不到指定的模組。
  於 SQLitePCL.NativeLibrary.TryLoad(String name, Loader plat, Action`1 log, IntPtr& h)
NOT FOUND

訊息提供了重要線索,Temporary ASP.NET Files 下目錄找不到 e_sqlite3.dll 導致爆炸,確認改用 SQLitePCLRaw.bundle_e_sqlite3 v2.0.7 後 bin 目錄有產生 runtimes 及 e_sqlite3.dll,但沒被複製到 ASP.NET 動態編譯目錄:

調查過程發現了案外案,SQLitePCLRaw.bundle_e_sqlite3 只要升級到 v2.1+,.NET Framework 專案編譯時不會產生 runtimes 目錄,查了一下,應該是原本的 packages\SQLitePCLRaw.lib.e_sqlite3.2.0.7\build\net461\SQLitePCLRaw.lib.e_sqlite3.targets 變成 packages\SQLitePCLRaw.lib.e_sqlite3.2.1.0\buildTransitive\net461\SQLitePCLRaw.lib.e_sqlite3.targets,buildTransitive 是新推出的 NuGet 機制,貎似跟 .NET Framework 水土不服(在 .NET Core/.NET 6 是好的,不然早炸鍋了),這段我還沒參透,先用 v2.0.7 避開問題。

既然知道問題出在 bin 目錄有 runtimes 但沒複製到 Temporary ASP.NET Files 目錄,爬文找到社群朋友分享的 Workaround,加一小段程式將 bin/runtimes 複製到實際 SQLitePCL.raw.dll 所在位置,原來的版本有點冗長,我改寫成較簡潔的版本:

public class SqliteDllFix
{
    public static bool IsPatched { get; private set; }

    public static void Patch()
    {
        if (IsPatched) return;
        var dstDir = Path.GetDirectoryName(typeof(SQLitePCL.raw).Assembly.Location);
        var srcDir = Path.GetDirectoryName(new Uri(typeof(SQLitePCL.raw).Assembly.CodeBase).AbsolutePath);
        var runtimeDir = Path.Combine(srcDir,"runtimes");
        foreach (var path in Directory.GetFiles(runtimeDir, "*.dll", SearchOption.AllDirectories))
        {
            var relPath = Regex.Match(path, "runtimes.+$").Value;
            var dstPath = Path.Combine(dstDir, relPath);
            Directory.CreateDirectory(Path.GetDirectoryName(dstPath));
            File.Copy(path, dstPath, true);
        }
        IsPatched = true;
    }
}

在 Global.asax.cs 或 App_Start/RouteConfig.cs 等初始物件呼叫 SqliteDllFix.Patch(),終於解決掉這個難搞的茶包。

小結一下 .NET Framework 使用 Microsoft.Data.Sqlite 的注意事項:

  1. Microsoft.Data.Sqlite v2.2 版以前版本可直接執行
  2. 若使用 Microsoft.Data.Sqlite v3.0+,SQLitePCLRaw.bundle_e_sqlite3 請升級到 v2.0.7 (但勿升級到 v2.1 以上,實測 v2.1+ 與 .NET Framework 有相容問題)
  3. 針對 ASP.NET 及單元測試專案,因實際執行位置與編譯檔輸出位置不同,即使升級 v2.0.7 仍會出現找不到 e_sqlite3 錯誤,加一段自己複製檔案過去的程式修正

解完問題有個心得:.NET Framework 已開始散發古蹟味,搭配新版元件很容易遇到 .NET 6 / ASP.NET Core 開發者不會踩到的坑。老系統維護人員多半沿用舊架構修修改改,很少會想融入新東西,因此這類古今合壁的另類做法格外顯得另類,參考資料相對少很多,想走這條路要有自立自強的心理準備。哈。

Tips of how to use Microsoft.Data.Sqlite v6.0 in .NET Framework 4.8


Comments

# by 小黑

謝黑哥

# by Yu

太强了哥,总结得好

Post a comment