The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

取得 .NET 8 應用程式的版本資訊必須注意的兩三事

我前陣子幫客戶開發一個 Windows Forms 應用程式,使用 .NET 8 來開發,不過在案子的最後一刻,客戶要求要加上一個「版本資訊」的功能,這個功能要顯示目前程式的版本號。這個功能實在是不常寫,沒想到還真的有點地雷,因為跟 .NET Framework 有點不太一樣了。這篇文章就來分享一下我在開發過程中遇到的問題,以及解決方法。

An abstract, conceptual wide banner illustrating the process of troubleshooting and resolving issues in  NET 8 Windows Forms applications

我的第一版實作

首先,我先在 *.csproj 加入 <FileVersion>0.1.0.0</FileVersion> 屬性:

<PropertyGroup>
  <OutputType>WinExe</OutputType>
  <TargetFramework>net8.0-windows</TargetFramework>
  <Nullable>enable</Nullable>
  <UseWindowsForms>true</UseWindowsForms>
  <ImplicitUsings>enable</ImplicitUsings>
  <Configurations>Debug;Release;LOCALDEV</Configurations>

  <FileVersion>0.1.0.0</FileVersion>

</PropertyGroup>

然後我使用 Assembly.GetExecutingAssembly().Location 來取得 DLL 組件的檔案路徑,然後再用 FileVersionInfo.GetVersionInfo 來取得版本號。

string? GetVersionInfo()
{
    // Get the executing assembly
    Assembly assembly = Assembly.GetExecutingAssembly();

    // Get the location of the assembly
    string location = assembly.Location;

    // Get the FileVersionInfo for the assembly
    FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(location);

    // Get the file version
    return fileVersionInfo.FileVersion;
}

在交付之前,我當然有測試過,這個方法在我的開發環境上是可以正常運作的。

不過,當我把應用程式發佈給客戶後,程式竟然會掛掉,因為我的發佈方式採用了 SCD (Self-Contained Deployment) 部署方法,而且把所有檔案全部都打包成單一檔案:

dotnet publish -c DEBUG --nologo --self-contained -r win-x64 -p:PublishSingleFile=true -p:DebugType=embedded -p:IncludeNativeLibrariesForSelfExtract=true

執行時的錯誤訊息如下:

Unhandled exception. System.ArgumentException: The path is empty. (Parameter 'path')
   at System.IO.Path.GetFullPath(String path)
   at System.Diagnostics.FileVersionInfo.GetVersionInfo(String fileName)

查了一下問題後才驚覺,原來用了 -p:PublishSingleFile=true 發佈為單一檔案時,這個 Assembly.GetExecutingAssembly().Location 會得到空字串,所以 FileVersionInfo.GetVersionInfo 才會拋出例外。

事實上,我在進行建置/發佈的時候,Console 有出現以下警告,不過我當時沒有留意:

warning IL3000: 'System.Reflection.Assembly.Location' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'.

這段訊息講的蠻清楚的,不過 System.AppContext.BaseDirectory 僅包含了「資料夾」路徑,並不包含「執行檔」的路徑,所以我還是無法取得版本號。

解決方法

我深入的瞭解了一下,發現有好幾個方法可以取得執行檔的路徑,我頓時陷入了選擇障礙:

  1. Environment.ProcessPath

    這個靜態屬性是從 .NET 6 開始才有的,可以取得目前正在執行的「執行檔」路徑,不過使用上要小心。

  2. System.Reflection.Assembly.GetEntryAssembly().Location

    當一個應用程式啟動時,這個方法會返回最初啟動應用程式時的那個組件(DLL),且通常是包含應用程式的 Main 函式的那個執行程序。而 .Location 屬性會回傳該檔案的路徑。

  3. System.Reflection.Assembly.GetExecutingAssembly().Location

    當一個函式被呼叫時,此方法將返回該函式所在的那個組件(DLL)。

由於我的需求是想取得「應用程式」的版本號,所以比較正確的選擇,應該只有以下兩種:

  1. Environment.ProcessPath
  2. System.Reflection.Assembly.GetEntryAssembly().Location

有趣的事情來了,我用不同的方式部署與執行,會得到不同的結果:

  1. 使用自封式部署 (SCD)

    dotnet publish -c DEBUG --nologo --self-contained -r win-x64 -o dist1
    

    若這樣執行:

    dist1\MyApp.exe
    

    這時 Environment.ProcessPath 會回傳 dist1\MyApp.exe,而 System.Reflection.Assembly.GetEntryAssembly().Location 會回傳 dist1\MyApp.dll

    若這樣執行:

    dist1\MyApp.exe
    

    這時 Environment.ProcessPath 會回傳 C:\Program Files\dotnet\dotnet.exe,而 System.Reflection.Assembly.GetEntryAssembly().Location 會回傳 dist1\MyApp.dll

    如果要抓版本的話,要使用 System.Reflection.Assembly.GetEntryAssembly().Location 才抓版本才是對的!👍

  2. 使用自封式部署 (SCD) 並且使用 PublishSingleFile 選項發佈單一執行檔

    dotnet publish -c DEBUG --nologo --self-contained -r win-x64 -p:PublishSingleFile=true -p:DebugType=embedded -p:IncludeNativeLibrariesForSelfExtract=true -o dist2
    

    若這樣執行:

    dist2\MyApp.exe
    

    這時 Environment.ProcessPath 會回傳 dist2\MyApp.exe,而 System.Reflection.Assembly.GetEntryAssembly().Location 則會回傳 "" 空字串。

    如果要抓版本的話,要使用 Environment.ProcessPath 才抓版本才是對的!👍

不同的發佈方法,會有完全不同的判斷結果,這真的是一個地雷。

不過,在徹底釐清上述特性後,我終於寫出了一個滿意的版本:

string GetVersionInfo()
{
    var location = System.Reflection.Assembly.GetEntryAssembly()?.Location;

    if (string.IsNullOrEmpty(location))
    {
        location = Environment.ProcessPath;
    }

    if (!string.IsNullOrEmpty(location))
    {
        FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(location);
        return fileVersionInfo.FileVersion ?? "";
        // fileVersionInfo.FileVersion 屬性為 string? 型別,但應該不可能沒有檔案版本,因為 .NET 有預設值 1.0.0.0
    }
    else
    {
        return "";
    }
}

這樣就可以在任何情況下都能正確取得版本號了!👍

結語

在 .NET 有很多方法可以取得當前程式所在的路徑,但是不同的取得方法都有些非常細微不同的地方:

  • Environment.CurrentDirectory 當前工作目錄
  • Directory.GetCurrentDirectory 當前工作目錄
  • System.AppContext.BaseDirectory 取得組件解析器用來探測組件的基礎目錄的檔案路徑
  • Assembly.GetCallingAssembly().Location 呼叫當前函式的函式所在的組件檔案路徑
  • Assembly.GetEntryAssembly().Location 程式進入點的那個組件的檔案路徑
  • Assembly.GetExecutingAssembly().Location 包含當前函式的組件檔案路徑

之所以會有多個相似的路徑,肯定是在不同的時空背景下有著不同的用途,所以我們在寫程式的時候,絕對不能以「我的電腦沒問題」來含糊帶過,若有這樣的想法,那就是阻礙你進步的元兇。

我在面試工程師的時候,也發現到了這個現象,很多人寫 Code 寫了很多年,但是對於一些基本的問題,卻無法回答出來。我並不是說這些人不會寫 Code,也不是說這些人沒有經驗,大家能在這個業界待個 5 ~ 10 年,肯定都有一定的解決問題能力,至少找答案的能力可能不差。但是,當面臨到需要提供高品質軟體的專案時,這些人就會顯得力不從心,因為他們的基礎知識不夠扎實,寫 Code 的當下無法保證是對的,甚至於沒意識到有其他可能的錯誤,進而導致無法穩定產出高品質的軟體。

這一年多來,生成式 AI 發展的非常快,但我聽到許多人抱怨 ChatGPT 有太多的幻覺問題,而且是所有 LLM (大語言模型) 都擁有程度不一的幻覺問題。不過,你真的認真思考一下就會發現,其實「人類」的幻覺才真的超級嚴重!

不相信嗎?你只要請任何一位從業多年的軟體工程師解釋一個抽象概念程式碼的運作原理時,最好在面試的時候問,因為他們為了求表現,一定會想辦法說出自己的「理解」,即便理解是錯誤的,他們也是會很有自信的說出他認為的「觀念」或「邏輯」,但他們自己不會覺得那是「幻覺」啊!😆

有趣的是,有時候我分享我的觀念給對方時,還有人會堅決不認為自己有錯,因為那是他多年來堅若磐石的理解。我有時候也不見得會認為自己永遠都是對的,但我自己如果有幻覺,我自己是不可能會知道的對吧?沒錯,那 ChatGPT 也一樣!所以怎麼辦呢?我覺得最好的方法,就是參與社群活動,多與人接觸,不要自己一個人閉門造車,將所學知識分享出來,讓別人來檢驗你的觀念,透過不斷的正向回饋,你的幻覺問題就會越來越少,也才能夠不斷的進步。👍

所以,我們在寫程式的時候,應該要有一個好習慣,將不理解的事情寫下來,然後等有空的時候再去查。老實說,我這樣的筆記有好幾千份,所以我很多事情都只是略懂,要等有空的時候才會去深入瞭解細節。

相關連結

留言評論