記將一個大型客戶端應用專案遷移到 dotnet 6 的經驗和決策

lindexi發表於2022-05-05

在經過了兩年的準備,以及遷移了幾個應用專案積累了讓我有信心的經驗之後,我最近在開始將團隊裡面最大的一個專案,從 .NET Framework 4.5 遷移到 .NET 6 上。這是一個從 2016 時開始開發,最多有 50 多位開發者參與,程式碼的 MR 數量過萬,而且整個團隊沒有一個人能說清楚專案裡面的所有功能。此專案引用了團隊內部的大量的基礎庫,有很多基礎庫長年不活躍。此應用專案當前也有近千萬的使用者量,遷移的過程也需要準備很多補救方法。如此複雜的一個專案,自然需要用到很多黑科技才能完成到 .NET 6 的落地。本文將告訴大家這個過程裡,我踩到的坑,以及學到的知識,和為什麼會如此做

前文

準確來說,我在這個過程其實算是最後一公里,我估算了工作量,大概將這個專案從 .NET Framework 4.5 遷移到 .NET 6 上的工時約 1.5 年人。雖然我現在說的是我用了五週的時間就完成了,但實際上在此前的準備工作是沒有被我算上的。此前的工作包括什麼?還包括將各大基礎庫更改為支援 dotnet core 的版本,填補 dotnet core 和 dotnet framework 的差異,例如 .NET Remoting 和 WCF 等 IPC 的缺失。更新打包平臺和構建平臺,使支援 dotnet core 的構建和打包。更新軟體的 OTA 也就是軟體自動更新功能,用於支援複雜的灰度釋出功能和測試 .NET 6 環境支援。逐步從邊緣到核心,逐個應用專案遷移,進行踩坑和積累經驗

在做足了準備之後,再加上足量的勇氣,以及一個好的時機,在整個團隊的支援下,我就開始進行最後一公里的遷移

其實在進行最後的從 .NET Framework 4.5 換到 .NET 6 之前,整個團隊包括我都是完全沒有想到還有如此多的坑需要填的。這個龐大的專案用了多少奇奇怪怪的黑科技還是沒有人知道的。在記錄本文時,我和夥伴們說,也許世界上沒有其他的團隊也會遇到我們的問題了

背景

一個從 2016 時開始開發,最多有 50 多位開發者參與,而且這些開發者們沒幾位是省油的,有任何東西都需要自己造的開發者,有任何東西只要能用別人做好的絕不自己造的開發者,有寫程式碼上過央視的開發者,有參與制定國家標準的開發者,有一個類裡面一定要用滿奇特的設計模式的開發者,有在程式碼註釋裡面一定要放大佛的開發者,有學到啥黑科技就一定要用上的開發者,有隻要程式碼和人一個能跑就好的開發者,有睜著眼睛說瞎話程式碼和註釋完全是兩回事的開發者,有程式碼註釋是文言文的開發者,有程式碼註釋是全英文的開發者,有註釋和文件遠超過程式碼量的開發者,有中文還沒學好的開發者,有喜歡挖坑而且必須自己踩的開發者,有啥東西都需要加日誌的開發者,有十分帥穿著西裝寫程式碼的開發者,有穿著女裝寫程式碼的開發者,有在程式碼裡面賣萌的開發者,有 這個函式只有我才能呼叫 的開發者,有相同的邏輯一定要用不同的方式實現的開發者,有在奔跑的坦克上換引擎的開發者

在本次遷移的過程,還有一些坑需要填。其中一個就是 dotnet core 裡面,沒有一個多 Exe 入口的客戶端應用的最佳實踐。這裡面涉及到客戶端應用獨立管理執行時環境時,多個 Exe 的衝突處理和安裝完成之後的資料夾體積的矛盾。這個也是本文分享的重點

本次還帶了一些需求,包括: 在確定系統環境滿足的情況下,低限度依賴系統,且需要做到不會被使用者系統上所安裝的 dotnet 執行時所影響。另外,考慮到後續要支援產品線內多個應用都共用執行時,但此執行時不能和其他團隊,其他公司所共有避免被魔改,還需要進行一些嘗試邏輯。最後,對使用的 WPF 版本是要求定製的,也就是說需要在官方釋出版本的基礎上,更改部分邏輯,滿足特殊的產品需求

這就意味著將 dotnet 重新分發,設定為團隊完全控制的庫。這個變更之後,在更新到 .NET 6 之後,可以執行完全的自主控制 dotnet 框架,包括 WPF 框架。於是可以做的事情就更加多了,無法實現的東西就更少了

為了做到對 WPF 更多的定製化,我將 WPF 框架的地位從原先的應用執行時層,更改為基礎庫層,地位和 團隊裡面的基礎元件 等 CBB 相同,只是作為底層庫而存在,架構上和 最底層的基礎庫 平級

本次遇到的問題分為兩個大類,一個是此專案本身的複雜度帶來的問題,另一個是 dotnet 帶來的問題。本文只記錄 dotnet 所帶來的問題,其中更多部分是因為特殊需求定製而導致問題

開發架構

原本的應用開發架構上,所依賴的 .NET Framework 是作為系統元件的存在。系統元件受到系統環境的影響,在國內妖魔鬼怪的環境下,系統元件被魔改被損壞是常態。採用 .NET Framework 的應用有著很大的客服成本,需要幫助使用者解決環境問題。隨著使用者量越來越大,這部分的客服成本也越來越大。這也就是為什麼有能投入到如此多資源來更新專案的原因之一

原本的應用開發架構分層如下圖

在更新到 dotnet 之後,執行時是在系統層的上方。如此的設計即可減少系統環境的影響,解決大量的應用環境問題

從上圖可以看到 WPF 是作為執行時的部分存在,但這不利於後續對 WPF 的定製化。我所在的團隊期望能完全將 WPF 進行控制,對 WPF 框架做深度定製。當然,本身團隊也有此能力,因為我也算是 WPF 框架的官方開發者。這部分深度的定製將會根據定製的不同,部分進行開源

變更後當前的開發架構分層如下圖

讓 WPF 作為基礎庫的一部分而存在,而不再放入執行時裡面。計劃是產品項裡面的多個產品專案是共用 .NET 執行時,單個各個產品之間自己帶 WPF 的負載,作為基礎庫

所遇到的問題

在進行最後一公里的更新就遇到了一些 dotnet core 機制上沒有最佳實踐的問題

多 AppHost 入口應用的依賴問題

多 Exe 應用的客戶端依賴問題是其中的一個機制性問題。當前正在遷移的專案是一個多程式模型的應用,有很多 Exe 的存在。然而 dotnet core 當前沒有一個最佳實踐可以讓多個 Exe 之間完美共享執行時且不受系統所安裝的全域性 dotnet 執行時影響,同時照顧到安裝完成之後的資料夾體積

我列出的問題點如下

  • 多個 Exe 檔案之間,如何共享執行時,如果不共享資料夾,各自獨立釋出,那將讓輸出資料夾體積非常大
  • 多個 Exe 檔案,如果在相同的資料夾進行釋出,將會相互覆蓋相同的名字的程式集。根據 dotnet 的引用依賴策略,如果有版本不相容情況,將出現 FileLoadException 錯誤
  • 不能使用 Program File 共享的全域性程式集,因為這個資料夾裡面的內容可能被其他公司的應用更改從而損壞,無法使用 dotnet core 環境獨立的能力
  • 不能使用 Program File 共享的全域性程式集,因為團隊內將會對 dotnet 執行時進行定製,例如定製 WPF 程式集,將 WPF 的地位從執行時更改為基礎庫。這部分定製不能汙染其他應用
  • 釋出到使用者端的執行時版本只能選用穩定的版本,而開發者會使用較新的 SDK 版本,開發構建輸出的程式集將引用較新 SDK 版本,如應用執行載入的只是釋出到使用者端的執行時版本,將會因為版本低於構建版本而出錯
  • 釋出到使用者端的執行時版本,是包含了定製版本的執行時,例如定製的 WPF 程式集。開發時應該引用定製的 WPF 程式集,但是不能引用低於構建版本的使用者端的執行時版本

另外由於 dotnet core 和 dotnet framework 對 exe 有機制性的變更,如 dotnet core 的 exe 只是一個 apphost 而已,預設不包含 IL 資料。而 dotnet framework 下預設 exe 裡面是包含應用入口以及 IL 資料程式集的。這就導致了原本的 NuGet 分發裡面有很多不支援的部分,好在這部分的坑踩平了

然而在進行 AppHost 的定製的時候,卻一定和 NuGet 分發進行衝突。由於 NuGet 是做統一的分發邏輯,如果在 NuGet 包上面帶 Exe 檔案,那一定此 Exe 檔案所配置的內容一定不符合具體的專案需求

依賴版本問題

在 dotnet 6 裡面,依賴和 .NET Framework 的尋找邏輯是不相同的,在 .NET Framework 只要存在同名的 DLL 即可,無視版本號。然而在 dotnet 6 裡面,卻實際的 DLL 的版本號要大於或等於依賴引用的 DLL 版本。核心問題衝突在於分發給使用者端的執行時框架版本,與開發者使用的 SDK 版本的差異

為什麼會出現此差異?原因是開發者使用的 SDK 基本都是最新的,然而分發給使用者端的執行時的版本是沒有勇氣使用最新的

想要理清此差異的問題,需要先理清概念

  • 開發者使用的 SDK 版本,也就是 dotnet 官方的 SDK 版本,大部分時候都使用最新的版本,例如 6.0.3 版本
  • 使用者端的執行時的版本,分發給到使用者的執行時版本,大部分時候都使用比較穩定的版本,例如 6.0.1 版本
  • 私有的版本,為了重新定製框架,例如給 WPF 框架加入自己的業務程式碼,由自己分發的版本。此版本也作為使用者端的執行時的版本,只是會基於一個穩定的 dotnet 官方釋出版本更改

在更新到 dotnet 6 之後,我們擁有了完全控制 dotnet 的能力,可以使用自己的私有的 dotnet 版本,當然 dotnet 版本也包括 WPF 版本。這就意味著可以對 WPF 框架進行足夠的定製化,在專案裡面使用自己定製化的 WPF 框架

然而使用自己定製化的 WPF 框架不是沒有代價的,將遇到分發給使用者端的執行時框架版本,與開發者使用的 SDK 版本的差異問題。此差異將會導致如果是分發的版本是私有的版本,這就意味著私有的版本一定落後開發者使用的 SDK 的版本。落後開發者使用的 SDK 的版本將會有兩個方面的問題

  1. 如果選用開發者的 SDK 版本作為軟體執行載入的程式集,那麼將因為不會載入到私有的版本的程式集,開發時無法使用到私有的版本。意味著私有的版本難以除錯,而且也無法在開發時處理私有的版本的行為變更
  2. 如果選用私有的版本作為軟體執行載入的程式集,那麼將因為私有的版本的版本號比開發者的 SDK 版本低,從而讓開發者構建出來的程式集找不到對應的版本從而執行失敗

當前處理方法

當前的處理方法是在開發時應用軟體的入口程式集裡面,加上對定製部分的程式集的引用,和輸出定製部分的程式集。如此可以在開發時使用私有的版本

在伺服器構建時,設定讓應用軟體的入口程式集不再對定製部分的程式集的引用,從而讓構建出來的所有程式集不包含對定製部分的程式集的引用;構建時將定製部分的程式集的引用放入到 runtime 資料夾內被 AppHost 引用

組織檔案

程式碼檔案組織

先將定製部分的程式集存放到程式碼倉庫的 Build\dotnet runtime\ 資料夾裡面,例如自定義的 WPF 框架就存放到 Build\dotnet runtime\WpfLibraries\ 資料夾裡面

接著將決定使用的 dotnet 執行時版本,放入到 Build\dotnet runtime\runtime\ 資料夾裡面,此 runtime 資料夾的組織大概如下

├─host
│  └─fxr
│      └─6.0.1
├─shared
│  ├─Microsoft.NETCore.App
│  │  └─6.0.9901
│  └─Microsoft.WindowsDesktop.App
│      └─6.0.9904
└─swidtag

接著將定製部分的程式集覆蓋 runtime 資料夾

輸出檔案組織

輸出檔案包含兩個概念,分別是安裝包安裝到使用者裝置上的安裝輸出資料夾和在開發時的輸出資料夾。這兩個方式是不相同的

安裝包安裝到使用者裝置上的安裝輸出資料夾,例如輸出到 C:\Program Files\Company\AppName\AppName_5.2.2.2268\ 資料夾

在輸出的資料夾的組織方式大概如下

├─runtime
│  ├─host
│  │  └─fxr
│  │      └─6.0.1
│  ├─shared
│  │  ├─Microsoft.NETCore.App
│  │  │  └─6.0.9901
│  │  └─Microsoft.WindowsDesktop.App
│  │      └─6.0.9904
│  └─swidtag
├─runtimes
│  ├─win
│  │  └─lib
│  │      ├─netcoreapp2.0
│  │      ├─netcoreapp2.1
│  │      └─netstandard2.0
│  └─win-x86
│      └─native
├─Resource
│
│ AppHost.exe
│ AppHost.dll
│ AppHost.runtimeconfig.json
│ AppHost.deps.json
│
│ App1.exe
│ App1.dll
│ App1.runtimeconfig.json
│ App1.deps.json
│
└─Lib1.dll

為什麼會將 Runtime 包含執行時的資料夾放入到應用裡面?基於如下理由:

  • 由於有多個 exe 的存在,使用獨立釋出是不現實的
  • 考慮到後續可能團隊內的多個應用都會共享一個執行時,而不是每個應用都自己帶,因此將執行時 Runtime 放入到一個公共資料夾是合理的,但由於現在還沒有穩定,先在應用內進行測試
  • 此 Runtime 資料夾是包含自己定製的內容,和 dotnet 官方的有一些不同,因此不能做全域性安裝

既然不合適做獨立釋出,也不合適放在 Program File 做全域性,那隻能放在應用自己的資料夾裡面。為了能讓放在應用自己的資料夾裡面的 Runtime 資料夾能被識別,就需要定製 AppHost 檔案,詳細請參閱如下部落格

開發時的輸出資料夾是給開發者除錯使用的,輸出的資料夾是 $(SolutionDir)bin\$(Configuration)\$(TargetFramework) 資料夾,如 Debug 下的 dotnet 6 是輸出到 bin\Debug\net6.0-windows 資料夾。在輸出的資料夾的組織方式大概如下

├─runtimes
│  ├─win
│  │  └─lib
│  │      ├─netcoreapp2.0
│  │      ├─netcoreapp2.1
│  │      └─netstandard2.0
│  └─win-x86
│      └─native
├─Resource
│
│ AppHost.exe
│ AppHost.dll
│ AppHost.runtimeconfig.json
│ AppHost.deps.json
│
│ App1.exe
│ App1.dll
│ App1.runtimeconfig.json
│ App1.deps.json
│
│ PresentationCore.dll
│ PresentationCore.pdb
│ PresentationFramework.dll
│ PresentationFramework.pdb
│ ...
│ PresentationUI.dll
│ PresentationUI.pdb
│ System.Xaml.dll
│ System.Xaml.pdb
│ WindowsBase.dll
│ WindowsBase.pdb
│
└─Lib1.dll

可以看到開發時的輸出的資料夾沒有包含 Runtime 資料夾,但是將定製的程式集放在輸出資料夾,例如上面的定製的 WPF 程式集內容。如此可以實現在開發時,除了定製的程式集,其他可以使用 SDK 的程式集。為什麼如此做,請參閱下文的原因

修改專案檔案

在入口程式集裡面,加上對 定製部分的程式集 的引用邏輯,例如對定製的 WPF 的程式集,也就是放在 Build\dotnet runtime\WpfLibraries\ 資料夾裡面的 DLL 進行引用和拷貝輸出

  <ItemGroup>
    <Reference Include="$(SolutionDir)Build\dotnet runtime\WpfLibraries\*.dll"/>
    <ReferenceCopyLocalPaths Include="$(SolutionDir)Build\dotnet runtime\WpfLibraries\*.dll"/>
  </ItemGroup>

如此即可實現在開發時,引用定製版本的程式集,輸出,從而除錯用到定製版本的程式集

這是 dotnet 的 SDK 的一個功能,判斷如果有和執行時框架存在的程式集已被引用,那麼將優先使用此程式集而不使用框架的程式集。這就是以上程式碼可以使用定製的 WPF 程式集替換 dotnet 的 SDK 帶的版本的基礎支援

由於在實際釋出的時候,在伺服器構建,為了減少在使用者安裝之後的資料夾體積,就期望不使用在入口程式集引用定製版本的程式集的輸出的檔案,只使用放在 runtime 資料夾的版本,減少重複的檔案。因此需要對入口程式集的引用程式碼進行優化,設定在伺服器構建時,不輸出

實現方法就是在伺服器構建時,通過 msbuild 引數,設定屬性,在專案檔案判斷屬性瞭解是否伺服器構建,如果是伺服器構建就不進行引用程式集

  <ItemGroup Condition=" '$(TargetFrameworkIdentifier)' != '.NETFramework' And $(DisableCopyCustomWpfLibraries) != 'true'">
    <Reference Include="$(SolutionDir)Build\dotnet runtime\WpfLibraries\*.dll"/>
    <ReferenceCopyLocalPaths Include="$(SolutionDir)Build\dotnet runtime\WpfLibraries\*.dll"/>
  </ItemGroup>

通過 msbuild 引數修改構建詳細請看下文

以上的方法存在設計的缺陷,那就是開發者使用的邏輯將和實際在使用者執行的不相同,但是我也沒有找到其他的方式可以解決如此多的問題

修改構建

在伺服器構建時,傳入給 msbuild 的引數,加上 /p:DisableCopyCustomWpfLibraries=true 配置不要引用自定義版本的 WPF 框架

然後在構建的時候,需要從 Build\dotnet runtime\runtime\ 資料夾拷貝定製的執行時放入到輸出資料夾裡面

    /// <summary>
    /// 使用自己分發的執行時,需要從 Build\dotnet runtime\runtime 拷貝
    /// </summary>
    private void CopyDotNetRuntimeFolder()
    {
        var runtimeTargetFolder = Path.Combine(BuildConfiguration.OutputDirectory, "runtime");
        var runtimeSourceFolder =
            Path.Combine(BuildConfiguration.BuildConfigurationDirectory, @"dotnet runtime\runtime");
        PackageDirectory.Copy(runtimeSourceFolder, runtimeTargetFolder);
    }

也就是說不讓入口程式集引用自定義版本的 WPF 框架,而是換成讓應用執行去引用 runtime 資料夾裡面的,從而減少重複的檔案

決策原因

以上的解決方法是有進行復雜的決策,下面來告訴大家每個決策的原因

解決多個 Exe 檔案之間共享執行時

多個 Exe 檔案,而且有 Exe 存放在其他資料夾,如 Main 資料夾等。這些 Exe 如果都進行獨立釋出,那安裝的輸出資料夾體積很大,而且重複檔案也很多,構建也需要慢慢等

解決方法是通過 AppHost 的定製的方式,讓所有的 Exe 都載入應用輸出資料夾的 runtime 資料夾的內容。如此可以實現多個 Exe 檔案之間共享執行時

為了能讓放在應用自己的資料夾裡面的 Runtime 資料夾能被識別,定製 AppHost 檔案,詳細請參閱如下部落格

除進行定製 AppHost 檔案去識別 Runtime 資料夾之外,第二個方案,另一個方法是修改檔案組織結構,最外層稱為 Main 入口應用資料夾,只放主入口 Exe 檔案及其依賴和執行時,而其他的 Exe 都放在裡層資料夾。要求放在裡層資料夾的 Exe 不能直接被外部執行,而是隻能由外層的入口 Exe 進行間接呼叫。在外層的入口 Exe 啟動里程資料夾的 Exe 的時候,通過環境變數告知里程資料夾的 Exe 的 dotnet 機制去使用到最外層稱為 Main 入口應用資料夾的執行時內容

然而第二個方案在本次遷移過程中沒有被我選擇,根本原因就是有很多古老且邊界的邏輯,這些邏輯有十分奇怪的呼叫方式。將原本的 Exe 放入到裡層資料夾,自然就修改了 Exe 的相對路徑,也許這就會掛了一堆業務模組。再有一部分 Exe 是被其他應用軟體啟動的,這部分也屬於改不動的。由於這些需求的存在,選擇將 Runtime 資料夾放在更外層,改 AppHost 檔案,讓這些可執行程式檔案之間共享同一個私有部署的 .NET 執行時

解決定製版本汙染全域性

對 dotnet 執行時的定製,例如定製 WPF 程式集,將 WPF 程式集的地位從執行時修改為基礎庫。這個定製更改的分發到使用者端有兩個方式

  • 帶給應用自己,例如應用獨立釋出
  • 全域性安裝到 Program File 裡面

為了不汙染到其他公司的應用,不能全域性安裝到 Program File 裡面。只能帶給應用自己

如上文,做每個 Exe 的獨立釋出是不合適的,只能放入到輸出資料夾的 runtime 資料夾

呼叫外掛程式

有外掛程式是放在 AppData 資料夾的,不在應用的安裝輸出資料夾裡面,如何呼叫外掛程式讓外掛程式可以使用到執行時,而不需要讓外掛自己帶一份執行時

實現方法是通過環境變數的方式,在 dotnet 裡面,將會根據程式的環境變數 DOTNET_ROOT 去找執行時

在主應用入口 Program 啟動給應用自己加上環境變數,根據 dotnet 的 Process 啟動策略,被當前程式使用 Process 啟動的程式,將會繼承當前程式的環境變數。從而實現了在使用主應用啟動的外掛程式,可以拿到 DOTNET_ROOT 環境變數,從而使用主應用的執行時

        /// <summary>
        /// 加上環境變數,讓呼叫的啟動程式也自動能找到執行時
        /// </summary>
        static void AddEnvironmentVariable()
        {
            string key;
            if (Environment.Is64BitOperatingSystem)
            {
                // https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-environment-variables
                key = "DOTNET_ROOT(x86)";
            }
            else
            {
                key = "DOTNET_ROOT";
            }

            // 例如呼叫放在 AppData 的獨立程式,如 CEF 程式,可以找到執行時
            var runtimeFolder =
                Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase!, "runtime");
            Environment.SetEnvironmentVariable(key, runtimeFolder);
        }

根據官方文件,對 x86 的應用,需要使用 DOTNET_ROOT(x86) 環境變數

詳細請看 dotnet 6 通過 DOTNET_ROOT 讓調起的應用的程式拿到共享的執行時資料夾

然而此方法也是有明確缺點的,那就是這些外掛自身是不能單獨執行的,單獨執行將找不到執行時從而失敗,必須由主入口程式或者其他拿到執行時的程式通過設定環境變數執行外掛才能正確執行

此問題也是有解決方法的,解決方法就是在不汙染全域性的 dotnet 的前提下,將 dotnet 安裝在自己產品資料夾裡面,預設的 Program File 裡面的應用資料夾佈局都是 C:\Program File\<公司名>\<產品名> 的形式。於是可以將 dotnet 當成一個產品進行安裝,於是效果就是如 C:\Program File\<公司名>\dotnet 的組織形式。如此即可以在多個應用之間通過絕對路徑共享此執行時

本次不採用資料夾佈局為 C:\Program File\<公司名>\dotnet 的組織形式去解決問題,是因為當前使用的 dotnet 管理方法,以及正在遷移版本過渡中,再加上使用的私有的 WPF 也沒有成熟,因此不考慮放在 C:\Program File\<公司名>\dotnet 的形式。而且也作為這個組織形式,需要考慮 OTA 軟體更新的問題,以及更新過程中出錯回滾等問題,需要更多的資源投入。但此方式可以作為最終形態

處理開發者的 SDK 版本比準備發給使用者的執行時的版本高的問題

遇到的問題: 開發者的 SDK 版本比準備發給使用者的執行時的版本高,此時構建出來的 DLL 將引用高版本的 .NET 的程式集,從而在開發者執行的時候,將會提示找不到對應版本的程式集

由於寫了 App.config 是無效的,因此無法使用之前的方式來將多個版本合為一個版本。正在尋找解決方法,但是依然沒有找到

嘗試的解決方法有兩個: 第一個是讓開發者安裝與使用者執行時的版本相同的 SDK 然後通過 global.json 設定特定的版本。這是可以解決的,只是需要開發者額外安裝 SDK 而已,安裝 SDK 的方法是解壓縮檔案

第一個方法需要給每個開發者安裝舊 SDK 版本,而且每次更新 SDK 都需要重新對每個開發者來一次。這對於新加入的開發者不友好,因為需要開發者部署環境。但是 dotnet 的 SDK 如果有新版本,是不能安裝舊版本的,除非是預覽版,這就讓開發者的部署比較複雜。這就是為什麼當前不使用第一個方法的原因

嘗試第二個方法: 在 入口程式集 裡面,引用 WPF 定製版本的程式集,此時將會在開發構建被輸出,在開發執行被引用。在釋出的時候,使用 runtime 資料夾下的內容,同時刪除輸出資料夾裡的內容

釋出的時候,使用 runtime 資料夾下的內容,同時刪除輸出資料夾裡的內容的原因是為了減少在使用者端的檔案體積,因為使用 runtime 資料夾下的內容和存放到程式集入口所在資料夾的定製版本的程式集檔案是完全相同。例如定製版本的 WPF 程式集釋出之後約 30M 左右,重複的檔案將多佔用使用者端的 30M 左右的空間,但這不影響安裝包的大小

第二個方法有缺點,每次釋出 WPF 私有版本,或者更新 .NET 版本,都需要手動拷貝檔案。也許後續版本可以考慮做 NuGet 分發包

第二個方法不能簡單刪除輸出資料夾裡的內容,而是需要在伺服器打包讓入口專案不做引用,否則將會因為 deps.json 檔案引用程式集被刪除,從而執行軟體失敗

以下是 deps.json 的配置引用程式集例子

 "PresentationFramework/6.0.2.0": {
        "runtime": {
          "PresentationFramework.dll": {
            "assemblyVersion": "6.0.2.0",
            "fileVersion": "42.42.42.42424"
          }
        },
        "resources": {
          "cs/PresentationFramework.resources.dll": {
            "locale": "cs"
          },
          "de/PresentationFramework.resources.dll": {
            "locale": "de"
          },
          "es/PresentationFramework.resources.dll": {
            "locale": "es"
          },
          "fr/PresentationFramework.resources.dll": {
            "locale": "fr"
          },
          "it/PresentationFramework.resources.dll": {
            "locale": "it"
          },
          "ja/PresentationFramework.resources.dll": {
            "locale": "ja"
          },
          "ko/PresentationFramework.resources.dll": {
            "locale": "ko"
          },
          "pl/PresentationFramework.resources.dll": {
            "locale": "pl"
          },
          "pt-BR/PresentationFramework.resources.dll": {
            "locale": "pt-BR"
          },
          "ru/PresentationFramework.resources.dll": {
            "locale": "ru"
          },
          "tr/PresentationFramework.resources.dll": {
            "locale": "tr"
          },
          "zh-Hans/PresentationFramework.resources.dll": {
            "locale": "zh-Hans"
          },
          "zh-Hant/PresentationFramework.resources.dll": {
            "locale": "zh-Hant"
          }
        }
      },

解決以上問題的方法就是如上的處理方法的做法,在開發者構建和伺服器構建使用不同的引用關係

處理使用者載入到全域性的程式集問題

背景

在 dotnet 裡面,將會進行版本評估,基於 Roll forward 進行策略邏輯,假設走的是預設的 Minor 的策略。優先尋找的是 AppHost 裡面記錄的 Runtime 資料夾,接著去尋找 Program File 的 dotnet 資料夾。取裡面一個合適的版本號,假如 應用 當前是採用 6.0.1 進行打包,而 Program File 裡面,使用者安裝了 6.0.3 的版本,那將會被選擇使用 Program File 的 6.0.3 的版本

這就意味著,如果使用者的 Program File 的 6.0.3 版本是損壞的,將會讓 應用 使用被損壞檔案

於是就達不到使用 dotnet 能處理環境問題

期望是能不在使用者端自動載入 Program File 這個全域性的程式集,而是使用應用自己帶的 runtime 資料夾的程式集

處理方法

讓 應用 的 Runtime 的 dotnet 的資料夾的版本號足夠高,即可解決此問題

更改放在 應用 的 Runtime 的 dotnet 的資料夾為 6.0.990x 版本,最後的 x 是對應原本 dotnet 官方的 Minor 版本號。如 6.0.1 對應 6.0.9901 版本號

根據 Roll forward 的邏輯,將會判斷 6.0.990x 版本是最高版本,從而不會載入 Program File 這個全域性的程式集

詳細請看 https://docs.microsoft.com/en-us/dotnet/core/versions/selection

除錯方法

進行修改 Runtime 資料夾載入路徑,是需要進行除錯的,由於開發者大部分情況下都有安裝好 SDK 環境,這也讓開發者無法很好的在自己的裝置上進行除錯。原因是如果自己的 Runtime 資料夾配置出錯,將讓 AppHost 預設載入進入了 SDK 環境,因此也就在開發者的裝置上可以符合預期的執行

然而在使用者的裝置上,沒有環境,或者是損壞的,那麼應用將跑不起來

一個在開發者裝置上除錯的方法是加上環境變數,通過 dotnet 自帶的 AppHost 除錯方式,將引用載入進行輸出

假設要測試的應用是 App.exe 檔案,可以開啟 cmd 先輸入以下命令,用於給當前的 cmd 加上環境變數,如此做可以不汙染開發環境

set COREHOST_TRACE=1
set COREHOST_TRACEFILE=host.txt

設定完成之後,再通過命令列呼叫 App.exe 檔案,此時的 App.exe 檔案將會輸出除錯資訊到 host.txt 檔案

App.exe

一個除錯資訊的內容如下

--- The specified framework 'Microsoft.WindowsDesktop.App', version '6.0.0', apply_patches=1, version_compatibility_range=minor is compatible with the previously referenced version '6.0.0'.
--- Resolving FX directory, name 'Microsoft.WindowsDesktop.App' version '6.0.0'
Multilevel lookup is true
Searching FX directory in [C:\lindexi\App\App\runtime]
Attempting FX roll forward starting from version='[6.0.0]', apply_patches=1, version_compatibility_range=minor, roll_to_highest_version=0, prefer_release=1
'Roll forward' enabled with version_compatibility_range [minor]. Looking for the lowest release greater than or equal version to [6.0.0]
Found version [6.0.1]
Applying patch roll forward from [6.0.1] on release only
Inspecting version... [6.0.1]
Changing Selected FX version from [] to [C:\lindexi\App\App\runtime\shared\Microsoft.WindowsDesktop.App\6.0.1]
Searching FX directory in [C:\Program Files (x86)\dotnet]
Attempting FX roll forward starting from version='[6.0.0]', apply_patches=1, version_compatibility_range=minor, roll_to_highest_version=0, prefer_release=1
'Roll forward' enabled with version_compatibility_range [minor]. Looking for the lowest release greater than or equal version to [6.0.0]
Found version [6.0.1]
Applying patch roll forward from [6.0.1] on release only
Inspecting version... [3.1.1]
Inspecting version... [3.1.10]
Inspecting version... [3.1.20]
Inspecting version... [3.1.8]
Inspecting version... [5.0.0]
Inspecting version... [5.0.11]
Inspecting version... [6.0.1]
Inspecting version... [6.0.4]
Attempting FX roll forward starting from version='[6.0.0]', apply_patches=1, version_compatibility_range=minor, roll_to_highest_version=0, prefer_release=1
'Roll forward' enabled with version_compatibility_range [minor]. Looking for the lowest release greater than or equal version to [6.0.0]
Found version [6.0.1]
Applying patch roll forward from [6.0.1] on release only
Inspecting version... [6.0.4]
Inspecting version... [6.0.1]
Changing Selected FX version from [C:\lindexi\App\App\runtime\shared\Microsoft.WindowsDesktop.App\6.0.1] to [C:\Program Files (x86)\dotnet\shared\Microsoft.WindowsDesktop.App\6.0.4]
Chose FX version [C:\Program Files (x86)\dotnet\shared\Microsoft.WindowsDesktop.App\6.0.4]

--- 開始,就是載入各個負載,如桌面等。開始讀取的尋找資料夾是放在 AppHost 裡面的配置,這是通過 在多個可執行程式(exe)之間共享同一個私有部署的 .NET 執行時 - walterlv 的方法設定的,讓應用去先尋找 runtime 資料夾的內容,如上文的檔案佈局

接著在 dotnet 裡面,讀取到的 Roll forward 策略是 minor 的值,接下來尋找到 6.0.1 版本,放在 runtime 資料夾的內容

'Roll forward' enabled with version_compatibility_range [minor]. Looking for the lowest release greater than or equal version to [6.0.0]
Found version [6.0.1]

作為第一個找到的內容,就將作為預設的執行時資料夾

Changing Selected FX version from [] to [C:\lindexi\App\App\runtime\shared\Microsoft.WindowsDesktop.App\6.0.1]

接著繼續尋找 C:\Program Files (x86)\dotnet 資料夾

Searching FX directory in [C:\Program Files (x86)\dotnet]

在全域性的資料夾找到了很多個版本,找到了很多個版本將和預設的執行時資料夾進行對比版本,找到最合適的一個

如上面程式碼,找到了 6.0.4 比預設的 6.0.1 更合適,於是就修改當前找到的執行時資料夾為 6.0.4 的版本

Changing Selected FX version from [C:\lindexi\App\App\runtime\shared\Microsoft.WindowsDesktop.App\6.0.1] to [C:\Program Files (x86)\dotnet\shared\Microsoft.WindowsDesktop.App\6.0.4]

由於沒有其他可以尋找的資料夾了,就將 6.0.4 作為使用的執行時資料夾

Chose FX version [C:\Program Files (x86)\dotnet\shared\Microsoft.WindowsDesktop.App\6.0.4]

通過此方式可以瞭解到自己讓應用找到的執行時資料夾符合預期

以上就是遷移此應用所踩到的坑,以及所採用的決策。希望對大家的遷移有所幫助

相關文章