ASP.NET Core 專案同時支援 2.x 與 3.x 的相關問題

在升級至 ASP.NET Core 3.x 的時候,為了讓專案繼續支援 .NET Core 2.x 的環境而碰到一些問題。這篇筆記要整理其中一個問題的由來,並說明如何使用條件編譯與類型別名來解決 ASP.NET Core 專案同時支援 2.x 和 3.x 所衍生的問題。

內容大綱:
  1. 一點歷史
  2. 兼容並蓄、異中求同
  3. 使用條件編譯與類型別名
  4. 在 csproj 檔案中引用不同版本的套件
  5. 結語

一點歷史

ASP.NET Core 2.1 引進了 Generic Host 來為那些不是基於 HTTP 的應用程式提供應用程式組態(configuration)、相依性注入(dependency injection)和紀錄(logging)等功能,而這些功能是透過 Microsoft.Extensions.* 相關組件來提供。如此一來,這些功能便有了兩份實作,而且有著類似的名稱(但命名空間不同)。

比如說,ASP.NET Core 程式裡面用的是 WebHostBuilder,在非 HTTP 的應用程式裡則是用 HostBuilder;這兩個類別所實作的介面分別是 IWebHostBuilderIHostBuilder,而這兩個介面(與其實作)是各自獨立的,彼此沒有任何繼承關係。IHostBuilder 來自 Microsoft.Extensions.Hosting 命名空間,而 IWebHostBuilder 來自 Microsoft.AspNetCore.Hosting。(看完這段,你是不是覺得有點暈?)

為了減少命名混亂和衝突的困擾,ASP.NET Core 3.0 開始統整這些概念相似但具有不同實作的組件。然而,在改革進步的同時,總得考慮這些新的改變是否會影響既有的 ASP.NET Core 2.x 應用程式,也就是說,必須向後相容(backward compatibility)。

即便有考慮跟舊版相容,升級至 ASP.NET Core 3.x 的過程還是難免碰到一些麻煩,症狀依應用程式的複雜度而不同:
  • 如果你的專案極少(或完全沒有)使用第三方套件,而且 target framework 只有一個(例如 netcoreapp2.1),那麼直接升級至 ASP.NET Core 3.x 應該是個簡單任務。
  • 如果你的專案使用了不少第三方套件,而且 target framework 只有一個,那麼主要就是看看那些相依套件是否能順利運行於 .NET Core 3.x 的環境。一般而言,升級套件時多少會需要修改一下既有的程式碼,但通常不是大問題(只是根據我的有限經驗來說)。
  • 如果你的 ASP.NET 程式使用了不少第三方套件,而且又必須支援多種 target frameworks(例如 netcoreapp2.1、netcoreapp3.1),這就會碰到比較多麻煩了。
微軟提供了一份詳細的文件,說明如何把 ASP.NET Core 2.x 應用程式升級到 3.x:Migrate from ASP.NET Core 2.2 to 3.0。另外也可以參考黑大的筆記:ASP.NET Core 2.2 升級 3.1 經驗一則

本文內容偏重在比較麻煩的情境,也就是支援多種 framework 版本(multi-targeting)的專案在升級至 ASP.NET Core 3.x 的時候可能碰到的問題。

兼容並蓄、異中求同

前面提到,如果 ASP.NET 程式使用了不少第三方套件,而且又得同時支援多種版本的 frameworks(例如 netcoreapp2.1、netcoreapp3.1),就會碰到比較多麻煩,因為 ASP.NET Core 3.0 開始統整一些相似概念的型別,以便消除重複和避免混淆,然而又得向後相容,最終只好在新版本當中推出新型別。那麼,既然 3.x 的某些型別名稱改成跟 2.x 不同(原本 2.x 的型別當然還是保留著),那麼當我們想要讓 ASP.NET Core 應用程式同時支援 2.x 和 3.x 的環境,勢必得費一番工夫。

拿我最近碰到的狀況為例,當我為 ASP.NET Core 專案的 target frameworks 增加 netcoreapp3.1 之後,便開始出現編譯警告。底下先列出我的 csproj 部分內容:


你可以看到,支援的 frameworks 包括 .NET Core 2.x 和 3.x,以及 .NET Framework 4.6.2。

編譯專案時,好幾個 .cs 檔案都出現編譯警告,訊息如下:
Warning CS0618: 'IHostingEnvironment' is obsolete: 'This type is obsolete and will be removed in a future version. The recommended alternative is Microsoft.AspNetCore.Hosting.IWebHostEnvironment.' MyApp.WebApi (netcoreapp3.1)
意思是,在針對 netcoreapp3.1 環境來編譯專案 MyAspNetApp.WebApi.csproj 時,發現程式裡面使用了過時的 IHostingEnvironment 型別,未來可能會拿掉它,故建議改用 IWebHostEnvironment

咦?那還不簡單,只要把程式裡所有的 IHostingEnvironment 替換成新的 IWebHostEnvironment 不就結了嗎?

對於單一 target framework 的專案來說,差不多就是這麼簡單。然而支援多種 framework 的專案則比較麻煩,不是單純替換字串就能解決(稍後會說明解決方法)。

過時的 IHostingEnvironment 有點討厭,因為它存在於兩個命名空間:Microsoft.AspNetCore.HostingMicrosoft.Extensions.Hosting 都各有一個 IHostingEnvironment,而且彼此雖然長得相似,卻互不相關(沒有繼承關係)。以下程式碼可以看出兩者的差異:


我不是很在意這些差異,畢竟它已經被打上「過時」的記號。重點是如何處理剛才的編譯警告。

另一方面,新的 IWebHostEnvironment 介面隸屬命名空間 Microsoft.AspNetCore.Hosting。該介面繼承自 IHostEnvironment,隸屬另一個命名空間:Microsoft.Extensions.Hosting。從這個小地方也可以發現,ASP.NET Core 3.0 以後的 hosting 功能已經是建立在非 HTTP(即開頭提過的 Generic Host)的基礎之上,不再是互不相干了。

使用條件編譯與類型別名

如果 ASP.NET 專案無需考慮 netcoreapp2.x,而只支援 netcoreapp3.x,那麼很簡單,只要把程式裡面的 IHostingEnvironment 全部替換成 IWebHostEnvironment 或其父代介面 IHostEnvironment 就行了。然而,我的專案需要同時支援 netcoreapp2.x 和 netcoreapp3.x,還是得在程式裡動點手腳。

先看一下修改前的程式碼:


我的作法是,在程式裡面使用 ASP.NET Core 3 的新型別:IHostEnvironment,並且用類型別名(alias)來讓程式碼同時支援 netcoreapp2.x 和 3.x 的編譯。如下所示:


用白話來解釋第 3 行至第 7 行程式碼,就是:
  • 在針對 netcoreapp3.1 環境來編譯時,引用新的命名空間 Microsoft.Extensions.Hosting(這樣才有新的介面 IHostEnvironment  可用)。
  • 若是針對其他 framework 來編譯,則給舊版的介面 Microsoft.AspNetCore.Hosting.IHostingEnvironment 添加新的別名:IHostEnvironment
如此一來,無論是 2.x 還是 3.x 版,寫程式的時候都一律使用 IHostEnvironment。底下用 kdiff3 來呈現程式碼修改前後的差異:


除了 IHostingEnvironment 介面,ASP.NET Core 3 還有新的 IHostApplicationLifetime 可取代過時的 IApplicationLifetime。我也是用同樣的方式來處理,底下是 GitHub 的差異比對結果:



在 csproj 檔案中引用不同版本的套件

除了在程式碼裡面使用條件編譯,你可能還會需要在 .csproj 檔案裏面使用條件式來引用不同版本的套件(理由同上,都是為了向後相容、支援多個 target frameworks)。像這樣:


欲了解更多細節,可參考微軟文件:SDK 樣式專案中的目標框架

值得一提的是,ASP.NET Core 2.x 的中繼套件(metapackage)Microsoft.AspNetCore.App 在 ASP.NETCore 3.0 以後已經不復存在。也就是說,並沒有 「Microsoft.AspNetCore.App v3.0.0」這樣的東西。那些 ASP.NET 應用程式中的共用套件,在 ASP.NET Core 3.0 以後都不會再出現於 nuget.org,而已經變成了框架內定的套件了。好處是,csproj 檔案變得清爽些,開發人員也更輕鬆一些,不用一直為那些個別套件的多種版本組合而傷腦筋。

結語

在升級的路上,如果是單一 target framework,事情會簡單許多,因為那些已經過時的套件或類別都可以直接丟棄,或改成新的名稱。

比較麻煩的情況,是在專案升級至 ASP.NET Core 3.x 時還要持續支援 ASP.NET Core 2.x(甚至更早之前的版本),再加上第三方套件所衍生的問題,往往得費一番工夫來處理。如果早晚都要面對,還是有時間就盡早動手升級吧(開一個新的分支來處理)。

Happy coding!

參考資料

沒有留言:

技術提供:Blogger.