在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 的版本將會有兩個方面的問題
- 如果選用開發者的 SDK 版本作為軟體運行載入的程式集,那麼將因為不會載入到私有的版本的程式集,開發時無法使用到私有的版本。意味著私有的版本難以調試,而且也無法在開發時處理私有的版本的行為變更
- 如果選用私有的版本作為軟體運行載入的程式集,那麼將因為私有的版本的版本號比開發者的 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 文件,詳細請參閱如下博客
- 在多個可執行程式(exe)之間共用同一個私有部署的 .NET 運行時 - walterlv
- 如何讓 .NET 程式脫離系統安裝的 .NET 運行時獨立運行?除了 Self-Contained 之外還有更好方法!談 dotnetCampus.AppHost 的工作原理 - walterlv
- 如何編譯、修改和調試 dotnet runtime 倉庫中的 apphost nethost comhost ijwhost - walterlv
開發時的輸出文件夾是給開發者調試使用的,輸出的文件夾是 $(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 文件,詳細請參閱如下博客
- 在多個可執行程式(exe)之間共用同一個私有部署的 .NET 運行時 - walterlv
- 如何讓 .NET 程式脫離系統安裝的 .NET 運行時獨立運行?除了 Self-Contained 之外還有更好方法!談 dotnetCampus.AppHost 的工作原理 - walterlv
- 如何編譯、修改和調試 dotnet runtime 倉庫中的 apphost nethost comhost ijwhost - walterlv
除進行定製 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])。