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

来源:https://www.cnblogs.com/lindexi/archive/2022/05/05/16226168.html
-Advertisement-
Play Games

在Excel中,可對單元格中的字元串設置多種不同樣式,通常只需要獲取到單元格直接設置樣式即可,該方法設置的樣式會應用於該單元格中的所有字元。如果需要對單元格中某些字元設置樣式,則可以參考本文中的方法。本文,將以C#及VB.NET代碼為例,介紹如何在Excel同一個單元格中應用多種字體樣式,包括字體加 ...


在經過了兩年的準備,以及遷移了幾個應用項目積累了讓我有信心的經驗之後,我最近在開始將團隊裡面最大的一個項目,從 .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]

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

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

博客園博客只做備份,博客發佈就不再更新,如果想看最新博客,請到 https://blog.lindexi.com/

知識共用許可協議
本作品採用知識共用署名-非商業性使用-相同方式共用 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發佈,但務必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含鏈接:http://blog.csdn.net/lindexi_gd ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。如有任何疑問,請與我[聯繫](mailto:[email protected])。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 需求 要求開發一個hello.go程式,可以輸出helloworld 開發的步驟 (1)開發這個程式/項目時,go的目錄結構怎麼處理 (2)代碼如下: ```go package main import "fmt" func main() { fmt.Println("hello,world") } ...
  • 前言 利用Python製作遠程查看別人電腦的操作記錄,與其它教程類似,都是通過郵件返回。 利用程式得到目標電腦瀏覽器當中的訪問記錄,生產一個文本併發送到你自己的郵箱,當然這個整個過程除了你把python程式植 入目標電腦外,其它的操作都是自動化的。 我知道我說到這些,有些人又開始有其它的想法了,可別 ...
  • #volatile關鍵字 ##什麼是可見性? 可見性是指線程A改變變數的值後,線程B可以馬上看到更改後變數的值 ##volatile的作用 關鍵字volatile提示線程每次從共用記憶體中讀取數據,而不是從私有記憶體中讀取,這樣就保證了同步數據的可見性 ##關鍵字volatile適用的場景 當想實現一個 ...
  • HandlerInterceptor源碼 ##3種方法: preHandle:攔截於請求剛進入時,進行判斷,需要boolean返回值,如果返回true將繼續執行,如果返回false,將不進行執行。一般用於登錄校驗。 postHandle:攔截於方法成功返回後,視圖渲染前,可以對modelAndVie ...
  • 在文章的開頭,先說下NPE問題,NPE問題就是,我們在開發中經常碰到的NullPointerException.假設我們有兩個類,他們的UML類圖如下圖所示 在這種情況下,有如下代碼 user.getAddress().getProvince(); 這種寫法,在user為null時,是有可能報Nul ...
  • 前言 大家都喜歡搞笑視頻倒放吧,視頻倒放會給人不一樣的感覺,比起按照原本的劇本,倒放的效果給人的感覺更出乎意料。所以, 我就想可不可以用Python實現視頻或者圖片倒放呢?於是,我做到了,我想把它分享給大家,讓大家跟我一起玩。 開始分享今天的技術之前,先來看幾個動圖(gif) (原圖1) (倒放1) ...
  • 寫文件有三種模式: 截斷寫,文件打開之後立即清空原有內容 附加寫,文件打開之後不清空原有內容,每次只能在文件最後寫入 覆蓋寫,文件打開之後不清空原有內容,可以在文件任意位置寫入 例如:文件原有內容為 123456,在開始位置覆蓋寫入 abcd,最後文件內容為 abcd56。首先使用下麵的代碼進行嘗試 ...
  • 1.什麼是Elasticserach? 一個由Java語言開發的全文搜索引擎,全文檢索就是根據用戶輸入查詢字元的片段,能查詢出包含片段的數據,簡單來說就是一個分散式的搜索與分析引擎,它可以完成分散式部署,結構化檢索,以及數據分析功能,主要是應用在微服務系統中。 我們使用大白話簡單的形式解釋,舉個例子 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:本文代碼示例演示瞭如何在WPF中使用LiveCharts庫創建動態條形圖。通過創建數據模型、ViewModel和在XAML中使用`CartesianChart`控制項,你可以輕鬆實現圖表的數據綁定和動態更新。我將通過清晰的步驟指南包括詳細的中文註釋,幫助你快速理解並應用這一功能。 先上效果: 在 ...
  • openGauss(GaussDB ) openGauss是一款全面友好開放,攜手伙伴共同打造的企業級開源關係型資料庫。openGauss採用木蘭寬鬆許可證v2發行,提供面向多核架構的極致性能、全鏈路的業務、數據安全、基於AI的調優和高效運維的能力。openGauss深度融合華為在資料庫領域多年的研 ...
  • openGauss(GaussDB ) openGauss是一款全面友好開放,攜手伙伴共同打造的企業級開源關係型資料庫。openGauss採用木蘭寬鬆許可證v2發行,提供面向多核架構的極致性能、全鏈路的業務、數據安全、基於AI的調優和高效運維的能力。openGauss深度融合華為在資料庫領域多年的研 ...
  • 概述:本示例演示了在WPF應用程式中實現多語言支持的詳細步驟。通過資源字典和數據綁定,以及使用語言管理器類,應用程式能夠在運行時動態切換語言。這種方法使得多語言支持更加靈活,便於維護,同時提供清晰的代碼結構。 在WPF中實現多語言的一種常見方法是使用資源字典和數據綁定。以下是一個詳細的步驟和示例源代 ...
  • 描述(做一個簡單的記錄): 事件(event)的本質是一個委托;(聲明一個事件: public event TestDelegate eventTest;) 委托(delegate)可以理解為一個符合某種簽名的方法類型;比如:TestDelegate委托的返回數據類型為string,參數為 int和 ...
  • 1、AOT適合場景 Aot適合工具類型的項目使用,優點禁止反編 ,第一次啟動快,業務型項目或者反射多的項目不適合用AOT AOT更新記錄: 實實在在經過實踐的AOT ORM 5.1.4.117 +支持AOT 5.1.4.123 +支持CodeFirst和非同步方法 5.1.4.129-preview1 ...
  • 總說周知,UWP 是運行在沙盒裡面的,所有許可權都有嚴格限制,和沙盒外交互也需要特殊的通道,所以從根本杜絕了 UWP 毒瘤的存在。但是實際上 UWP 只是一個應用模型,本身是沒有什麼許可權管理的,許可權管理全靠 App Container 沙盒控制,如果我們脫離了這個沙盒,UWP 就會放飛自我了。那麼有沒... ...
  • 目錄條款17:讓介面容易被正確使用,不易被誤用(Make interfaces easy to use correctly and hard to use incorrectly)限制類型和值規定能做和不能做的事提供行為一致的介面條款19:設計class猶如設計type(Treat class de ...
  • title: 從零開始:Django項目的創建與配置指南 date: 2024/5/2 18:29:33 updated: 2024/5/2 18:29:33 categories: 後端開發 tags: Django WebDev Python ORM Security Deployment Op ...
  • 1、BOM對象 BOM:Broswer object model,即瀏覽器提供我們開發者在javascript用於操作瀏覽器的對象。 1.1、window對象 視窗方法 // BOM Browser object model 瀏覽器對象模型 // js中最大的一個對象.整個瀏覽器視窗出現的所有東西都 ...