.NET 程序讀取當前目錄避坑指南
前些天有 AgileConfig 的用戶反映,如果把 AgileConfig 部署成 Windows 服務程序會啟動失敗。我看了一下日誌,發現根目錄被定位到了 C:\Windows\System32
下,那麼讀取 appsettings.json 配置文件自然就失敗了。
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory());
以上是我構造 ConfigurationBuilder 的代碼。使用 Directory.GetCurrentDirectory()
獲取程序根目錄然後設置 SetBasePath
。以上代碼在99%的情況是不會有問題的,那麼為什麼會在做為服務部署的時候會有問題呢?讓我們往下看。
Directory.GetCurrentDirectory()
Directory.GetCurrentDirectory()
獲取根目錄是我們很常見的一個操作。先讓我們對其進行一些簡單的測試。新建一個控制台程序,編寫以下代碼進行:
var dirpath = Directory.GetCurrentDirectory();
Console.WriteLine("Directory.GetCurrentDirectory = " + dirpath);
直接運行
代碼很簡單,就是讀取根目錄,然後輸出一下。
Directory.GetCurrentDirectory = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0
編譯完成後雙擊 exe 文件,可以看到獲取到的目錄是正確的。
使用 cmd 運行
下面讓我們試一下在 cmd 下運行這個 exe 。
C:\>cd C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0
C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0>basedir.exe
Directory.GetCurrentDirectory = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0
我們把路徑切換到 exe 所在的目錄,然後輸入 basedir.exe 直接運行它,可以看到輸出的目錄也是正確的。
切換工作目錄
這次我們把工作目錄切換到 C 盤的 apps 目錄,然後使用完全路徑去執行 exe 程序。
C:\>cd apps
C:\APPS>C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0\basedir.exe
Directory.GetCurrentDirectory = C:\APPS
怎麼樣?是不是跟預期的不一樣了?這次輸出的根目錄不是 exe 所在的目錄,而是 c:\APPS
,也就是我們的工作目錄。
使用另外一個 exe 程序啟動測試程序
在我們日常場景中有很多時候需要通過一個程序去運行另外一個程序,那麼這個時候 Directory.GetCurrentDirectory
獲取的根目錄是怎麼樣的呢?
首先我們編寫另外一個 WPF 的程序,使用這個程序來啟動我們的 basedir.exe 測試程序。
我們把這個 WPF 程序放在 c:\APPS
目錄下,然後運行它:
其中按鈕的事件代碼如下:
private void Button_Click(object sender, RoutedEventArgs e)
{
var process = new Process();
process.StartInfo.FileName = this.path.Text;
process.Start();
}
點擊這個按鈕,它會把我們的測試程序 basedir.exe 給運行起來:
我們可以看到,當 WPF 程序把我們的測試程序運行起來的時候,測試程序輸出的目錄為 c:\APPS
,也就是 WPF 程序所在的目錄。
為什麼做為服務運行的時候獲取程序根目錄為 System32
通過以上的測試,AgileConfig 做為服務運行的時候獲取根目錄為 C:\Windows\System32
的原因已經很明顯了。我們的 windows 服務的啟動一般來說有2個途徑,一個是通過 sc.exe 工具另外一個是通過 services.exe 也就是 SCM (service control manager) 來啟動。那麼這2個可執行程序在哪裡呢?答案就是 C:\Windows\System32
。我們的 windows 服務的啟動其實是通過這2個工具來運行的,所以根據上面的測試,很明顯通過Directory.GetCurrentDirectory
來獲取根目錄的話會是這2個工具所在的目錄。
其它讀取程序根目錄的方式
通過以上我們知道通過Directory.GetCurrentDirectory
讀取根目錄會有一點小坑。在我們的 .NET 世界裏還有很多辦法能獲取根目錄,那麼他們會不會也有坑呢?
以下列幾個常見的獲取根目錄的方法:
var dirpath = Directory.GetCurrentDirectory();
Console.WriteLine("Directory.GetCurrentDirectory = " + dirpath);
// 通過 AppDomain.CurrentDomain.BaseDirectory 讀取根目錄
var dirpath1 = AppDomain.CurrentDomain.BaseDirectory;
Console.WriteLine("AppDomain.CurrentDomain.BaseDirectory = " + dirpath1);
// 通過 Environment.CurrentDirectory 來讀取根目錄
var dirpath2 = Environment.CurrentDirectory;
Console.WriteLine("Environment.CurrentDirectory = " + dirpath2);
// 通過 Assembly.GetExecutingAssembly().Location 來獲取運行程序集所在的位置,從而判斷根目錄
var dirpath3 = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
Console.WriteLine("Path.GetDirectoryName Assembly.GetExecutingAssembly().Location = " + dirpath3);
// 通過 AppContext.BaseDirectory 獲取根目錄
var dirpath4 = AppContext.BaseDirectory;
Console.WriteLine("AppContext.BaseDirectory = " + dirpath4);
直接運行
把以上獲取根目錄的代碼補充進我們的測試程序,編譯成功後直接運行:
Directory.GetCurrentDirectory = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0
AppDomain.CurrentDomain.BaseDirectory = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0\
Environment.CurrentDirectory = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0
Path.GetDirectoryName Assembly.GetExecutingAssembly().Location = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0
AppContext.BaseDirectory = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0\
以上是輸出結果。可以看到所有的方法都準確的獲取到了 exe 程序所在的根目錄。有一點要注意的是
AppDomain.CurrentDomain.BaseDirectory
跟 AppContext.BaseDirectory
方法獲取的路徑最後帶有一個 \
其它則沒有。
使用 cmd 運行
同樣讓我們在 cmd 下運行一下:
C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0>basedir.exe
Directory.GetCurrentDirectory = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0
AppDomain.CurrentDomain.BaseDirectory = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0\
Environment.CurrentDirectory = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0
Path.GetDirectoryName Assembly.GetExecutingAssembly().Location = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0
AppContext.BaseDirectory = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0\
我們把路徑切換到 exe 所在的目錄,然後輸入 basedir.exe 直接運行它,可以看到所有的方法輸出的目錄都是正確的。
切換工作目錄
同樣我們在 cmd 下把工作目錄切換到 c:\APPS
,然後運行 exe 。
C:\>cd APPS
C:\APPS>C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0\basedir.exe
Directory.GetCurrentDirectory = C:\APPS
AppDomain.CurrentDomain.BaseDirectory = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0\
Environment.CurrentDirectory = C:\APPS
Path.GetDirectoryName Assembly.GetExecutingAssembly().Location = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0
AppContext.BaseDirectory = C:\00WORKSPACE\basedir\basedir\bin\Debug\net6.0\
可以看到 Directory.GetCurrentDirectory
和 Environment.CurrentDirectory
方法輸出均為 c:\APPS
而其它方法則都輸出了 exe 所在目錄。
使用另外一個 exe 程序啟動測試程序
同樣我們再次使用另外一個 WPF 程序來運行 basedir.exe 測試程序:
可以看到 Directory.GetCurrentDirectory
和 Environment.CurrentDirectory
方法輸出均為 c:\APPS
,也就是 WPF 所在的目錄, 而其它方法則都輸出了 exe 所在目錄。
總結
以上常見的 5 種讀取程序當前目錄的辦法在絕大多數情況下都可以正確的獲取到預期的結果。其中需要注意的是Directory.GetCurrentDirectory
和 Environment.CurrentDirectory
。這2個方法在 cmd 或者 bash 環境下返回的是工作目錄;使用 A 程序啟動另外一個 B 程序的時候,B 程序獲取到的根目錄是 A 程序所在的目錄。所以使用 Directory.GetCurrentDirectory
和 Environment.CurrentDirectory
的時候一定要格外注意,避免引入 BUG 。