雖然 .NET 6 正式推出已經是快一年前的事情了(現在況且都已經出到 .NET 7 了),但是因為很難評估把公司專案全部從 .NET Core 3.1 版升級過來的成本如何,所以升級的事情就一直拖到現在 3.1 快要結束支援週期了才終於找了一個空檔來處理。整體而言,升級的過程已經算是比我想像中的要少痛很多了(整個案子的所有相依專案大概有 20 個左右、都在兩天內就完成了升級),但是仍舊不能說是完全無痛。底下簡單分享一下我的升級流程以及遇到的一些坑。

升級流程

基本上,.NET 的程式原則上可以向下相容舊的套件,也就是說 .NET 6 的站台可以回頭去用 .NET Core 3.1 寫的套件、可是反過來 .NET Core 3.1 的站台卻不能越級去用 .NET 6 寫的套件。因此,升級的順序應該是從相依關係的最下層開始,逐一往上更新至底層套件。先把站台切換成 .NET 6,編譯看看能不能過,可以的話再把它相依的商業邏輯專案也跟著升級,依此類推。

新版的 .NET 一個很重要的新機制就是 null safety,但是在我升級的過程中我都不去碰這個,因為一旦啟用了這玩意兒,過去寫的程式碼就會有多到不行的地方必須一一修改,這暫時沒辦法塞進我們的專案排程之中。反正 null safety 不啟用也沒差、也不會有警告跑出來,我們就先繼續死撐到 C# 決定警告為止吧 😂

Reflection

升級過程中我第一個遇到的坑就是跟 Reflection 有關的程式碼。在我們的專案底層架構中有很多地方有用到 Reflection,其中一個例子是我做了像這樣的事情:

var methods = typeof(Queryable).GetMethods();
var propName = nameof(IRandomSerial<int>.Serial);
var propType = type.GetProperty(propName).PropertyType;
value = (T)(methods
    .First(m => m.Name == nameof(Queryable.Max) && m.GetParameters().Length == 2)
    .MakeGenericMethod(type, propType)
    .Invoke(null, new[] {
        set,
        ExpressionUtil.CreateMemberAccessExpression(type, propName)
    })
);

這邊其實我想要表達的是

value = set.Max<type, T>(e => e.Serial);

這樣一句話而已,但是因為 type 並不是當成泛型參數傳進來、而是作為 Type 類別的變數傳進來的,所以我只好用 Reflection 的寫法。

而為了正確鎖定 Max() 這個擴充方法,我用了 Linq 去篩選符合我認知的簽章的方法 1。原本這樣寫在 .NET Core 3.1 裡頭是沒問題的,不巧到了 .NET 6 之後,Queryable 竟然多了一個也叫做 Max() 的擴充方法、而且也是有兩個參數,這樣一來抓出來的方法就會是錯的。我必須額外在篩選條件中例如加入 && m.GetGenericArguments().Length == 2 才能修正這個錯誤。

但其實這樣的改法仍然不是最好的,因為誰知道以後 .NET 會不會又有多了新的多載擴充方法導致篩選條件又出錯?所以最根本的解決辦法應該是把這一段程式碼所在的方法直接改寫成泛型方法,然後把 Reflection 的部份改成是呼叫自己寫的方法、而非呼叫 Linq 的方法。換言之:

盡量把 Type 轉泛型的 Reflection 操作侷限在呼叫自己內部定義的方法上。

若干東西的棄用

有一些寫法到了 .NET 6 之後在編譯時就會出現該寫法被棄用(deprecated)的警告。當然,無視這些警告繼續使用也是一個選項,但是一方面我們在專案開發上是頗講究「至少程式碼要修到沒有警告為止」的,另一方面其實很多時候新的寫法反而看起來更順眼,所以就還是乖乖修正了。

我遇到的棄用寫法主要有兩個,一個是跟 System.Drawing 有關的程式碼。這些程式碼因為依賴於 GDI+ 程式庫,是有跨平台方面的顧慮的 2,所以 .NET 6 會建議不要使用,而改用真正跨平台的繪圖程式庫。我選擇的是 SixLabors.ImageSharp.Drawing 這一套,用起來跟 System.Drawing 滿接近的,語法雖然不一樣但是幾乎可以一一對應起來,所以遷移過程不算太麻煩。

另一個遇到的則是 WebRequest 的棄用;這個部份 .NET 6 會建議改採用 HttpClient,然而後者並不支援 FTP、偏偏我會需要用到,所以又要找套件來取代了。我最後選的是 FluentFTP,它的語法遠比起用 FtpWebRequest 來寫要簡潔,所以換過來反而舒服,就像我稍早提到過的。

Blazor 的若干改良

Blazor 從 3.1 進化到 6 版之後多了一些新的東西、也修正了一些問題,這使得某些我過去自行補完的措施顯得重複或者甚至會有衝突。其中一個最主要的就是內建了 <DynamicComponent> 這個過去大家都是自己寫的元件,只不過它的呼叫方式跟大家過去會寫的寫法都不一樣、不是直接把屬性寫上去,而是要透過一個 IDictionary<string, object> 去傳遞所有的屬性。雖然我個人並不喜歡這種作法,但既然都內建了,就還是用吧,未來接手的人也比較好維護。

另外有一個東西我不確定是不是 .NET 6 才修正、或者之前 3.1 版某個時期就已經修正了,但現在 <InputDate> 元件會正確地把產生出來的 <input> 標籤加上 type="date",這使得我自己補上去反而會跟該元件的泛型參數 Type 起衝突,所以要去掉。

相依套件有新的語法

專案升級到 .NET 6 之後,很多相依的第三方套件也會跟著升級到對應的 .NET 6 版本,然而有些套件升上去了之後其語法就發生了一些小改變,這當然得跟著改。

我遇到的主要有兩個:MySQL 套件的 UseMySql() 擴充方法的簽章改變了,必須多傳入一個 ServerVersion 進去;以及 MailKit 套件的 new MailboxAddress() 建構式的簽章要求多傳入一個收信人名稱。不算什麼太大的問題,反正編譯的時候都會自動報錯讓我知道。

EF Core 的新語法

EF Core 6 在定義資料模型時的語法也有不小的差異,所以如果有使用 EF Core Power Tools 的 Reverse Engineer 功能的話,最好是把 CodeTemplates 資料夾清空、讓該工具重新產生新的,再把自訂的部份補回去,這樣產生出來的語法才不會有錯。

最小裝載模型

.NET 6 當中簡化了站台程式進入點的語法成為所謂的「最小裝載模型(minimal hosting model)」。這個並非一定要用,專案升級成 .NET 6 之後也可以完全不去動原本的 Program.cs 和 Startup.cs 兩個檔案,但是如果想要改寫成新的語法的話,可以套用如下的範本。

// 檔案最一開始當然有一堆 using,看各位使用的東西而定。

var builder = WebApplication.CreateBuilder(args);

// 假如原本在 Program.cs 的 CreateHostBuilder 裡面,
// webBuilder 除了 UseStartup<Startup>() 之外還有額外設置 UseSomething(),
// 可以在這邊寫上 builder.WebHost.UseSomething()

var services = builder.Services;

// 把原本 Startup.cs 的 ConfigureServices() 的內容貼到這邊來

var app = builder.Build();

// 把原本 Startup.cs 的 Configure() 的內容貼過來。
// 原本的 env 參數出現的地方改成 app.Environment 即可。

app.Run();

用上述這一段最上層程式碼取代掉 Program.cs 的內容,然後刪除掉 Startup.cs 檔案即可。專案選項中的「隱含全域 Using」要不要勾都可以,勾的話可以少寫一些 using,但是可能會造成一些命名打架,要稍微注意。


  1. 據我所知,.NET 並沒有內建什麼方法是真正暴露出 C# 決定多載選取的完整機制的,所以只好自己去做篩選。在這個回答當中雖然有人給出了一套很接近 C# 機制的解答,但是即便其程式碼如此複雜、也還是沒有百分之百做到 C# 真正的程度。 

  2. 雖然也是有在 Linux 系統上面安裝 GDI+ 的方法(例如這篇),但是終究是多了一道設定上的工。 


分享此頁至:
最後修改日期: 2022/11/03

留言

感謝村哥您的回覆,非常的明確。 您有這種技術熱忱我真的非常佩服!! 絕對不是沒事顯得自己有做事而已哈哈 我再慢慢拜讀您的文章😀😀

Hi村哥你好, 我是Gino, 是從FB的.net社團連結過來的. 目前寫了四年的.NET程式, 主要是Framework4.7和.NET6, 沒有接觸到NETCore 3.1 的這個階段, 但我好奇升版這件事情~~ 在實務上專案中會遇到什麼需求導致會要做升版本的動作呀 ? 補充下, 因為目前接觸到的系統大多在專案部都會是比較希望穩定,減少工時,達到好績效. 甚至有些開發流程上都不是對工程師友善的, 所以相對我很好奇, 花這種工去升級專案版本, 我的想像目的應是為了程式的可用性與維護性, 使用相對新的主流的框架, 支援也會更多, 甚至也可以用更有效率的寫法. 但升級版本感覺會是一個使用者不會有感覺的一件事情. 已經RUN的專案需要多大的誘因才會想要做這件事

    其實如同你可以想像的,升級專案架構這種事情通常並沒有什麼迫切的動機;很多時候,程式只要表面上滿足了需求,不管背後的手段多老舊也沒差,而且就算一個版本的支援週期結束、想用當然也還是可以繼續用。我自己的經驗中,會想要升級既有的專案大概有幾個原因(依誘因排列):1. 新版提供了某種對老闆來說很誘人的新優勢(例如 Core 相對於 Framework 來說可以跨平台,改採用 Linux 主機可以降低公司的營運成本滿多的)2. 新版的功能對老闆來說沒差,但是對工程師來說有誘人之處(例如 .NET 6 的熱重載)3. 如同你說的,考慮到可維護性(未來接手的年輕工程師可能只會新版)4. 純粹就暫時沒有新的東西要開發,就來搞升級以顯得自己有在做事(很大的程度來說這才是我這次升級的最大原因 😂😂😂😂)

撰寫回覆或留言

發佈留言必須填寫的電子郵件地址不會公開。您的留言可能會在審核之後才出現在頁面上。