我的部落格這兩天悄悄從 .NET 5 升級到 .NET 6。

過程遇到 EF Core 2.2 升 6.0 產生的奇怪問題,原本以為要搞很久,但修掉兩個 Issue 後意外沒再發現其他問題,簡單測試主要功能都正常,我就當改好了直接上線全民公測,大家若有發現問題請再回報唄。

【本文開始】

2020 年底部落格從 ASP.NET Core 3.1 升到 5.0,.NET 6 在去年 11 月推出,拖了大半年,眼看 .NET 7 都快出來了,總算抽出時間把部落格平台再從 .NET 5 升級到 .NET 6。(明明是 .NET 5 升 6,這文章標題怎麼是 EF Core 2 升 3?就繼續看下去吧)

心想 .NET 5 到 6 改動不大,預期不需要改太多程式,不料升級之後,EF Core 部分直接爆炸,問題出在我的 EF Core 相關程式庫還在 2.2 版,用 .NET 5 可以跑,升到 .NET 6,一執行 ctx.Database.GetDbConnection() 便冒出錯誤,完全沒法連資料庫:

Exception thrown: 'System.TypeInitializationException' in Microsoft.EntityFrameworkCore.dll
The type initializer for 'Microsoft.EntityFrameworkCore.Query.ResultOperators.Internal.TrackingExpressionNode' threw an exception.
TypeInitializationException: The type initializer for 'Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions' threw an exception.
InvalidOperationException: Sequence contains more than one matching element

簡單,把 EF Core 2.2 升到 6.0 不就好了?於是我將幾個 EF Core 相關的 2.2.2 版程式庫升到 6.0.8:

升級後,資料庫可以連線了,但變成以下這段程式出錯:

ctx.Posts.FirstOrDefault(p => 
    p.Slug.Equals(slug, StringComparison.OrdinalIgnoreCase));

錯誤訊息指出 EF Core 6 無法將 string.Equals(value, StringComparison.OrdinalIgnoreCase) 轉譯成對映的 SQL 比對語法。媽呀,該不會有一堆不相容吧?

System.InvalidOperationException: 'The LINQ expression 'DbSet<Post>()
    .Where(p => p.Slug.Equals(
        value: __slug_0, 
        comparisonType: OrdinalIgnoreCase))' could not be translated. 
        Additional information: Translation of the 'string.Equals' overload with a 'StringComparison' parameter is not supported. 
        See https://go.microsoft.com/fwlink/?linkid=2129535 for more information. 
        Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly 
        by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. 
        See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'

本到官方文件,由於比對是否區分大小寫實務上是由 DB Collation 決定,更何況 string.Equals() 還有國別區域相關參數,C# 很難依據 DB 設定對映出正確邏輯;另一方面,若要強轉 Collation 再比對將導致無法使用 Index 加速,故新版 EF Core 只接受 Equals 轉成 SQL =。更進一步發現,這是 2.2 轉 3.1+ 就會遇到的差異,我撐到今天才遇到,噗。

SQLite 資料庫預設不分大小寫,故這段改成 p.Slug == slug 就算過關。

接著著冒出更離奇的錯誤。

post.Comments = ctx.Comments.Where(o => o.PostKey == post.PostKey)
    .OrderByDescending(o => o.PubDate).ToList();

冒出 Microsoft.Data.Sqlite.SqliteException: 'SQLite Error 1: 'no such column: c.PostKey1'.' 錯誤,不知為何 EF Core 在轉譯這段成 SQL 語法時會出現 c.PostKey 及 c.PostKey1 兩個欄位,後者並非 Comment 包含的欄位,是 EF Core 產生出來的。

爬文查到相似案例,一樣是 2.2 升 3.1 就該遇到的問題。推敲關鍵在於我的 Post 定義了 IList<Comment> Comments 屬性,Comment 有個 PostKey 欄位指向 Post 的 PostKey,但我沒明確定義 Post 與 Comment 的 Primary Key / Foreign Key 對映,EF Core 試著自己產生時出了差錯。參考 Stackoverflow 討論,在 DbContext 的 OnModelCreating 加入以下這段,問題解決。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // ... 略 ...
    modelBuilder.Entity<Post>().HasMany(p => p.Comments).WithOne().HasPrincipalKey("PostKey").HasForeignKey("PostKey");
    // ... 略 ...       
}

When upgrade my web site from ASP.NET Core 5.0 to 6.0, I got two issues from upgrading EFCore 2.2 to 3.1.


Comments

# by 樂透無名

眼花了,感謝指導

Post a comment