EF Core 8 的重大變更 (EF8)
此頁面記載 API 和行為變更,這些變更可能會中斷從 EF Core 7 更新至 EF Core 8 的現有應用程式。 如果要從舊版 EF Core 更新,請務必檢閱之前的重大變更:
目標 Framework
EF Core 8 的目標是 .NET 8。 以舊版 .NET、.NET Core 和 .NET Framework 版本為目標的應用程式必須更新為以 .NET 8 為目標。
摘要
高度影響變更
Contains
在 LINQ 查詢中,可能會停止在舊版 SQL Server 上運作
舊的行為
EF 對 LINQ 查詢具有專門化支援,透過參數化值清單使用 Contains
運算符:
var names = new[] { "Blog1", "Blog2" };
var blogs = await context.Blogs
.Where(b => names.Contains(b.Name))
.ToArrayAsync();
在 EF Core 8.0 之前,EF 會將參數化值插入 SQL 中:
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (N'Blog1', N'Blog2')
新的行為
從 EF Core 8.0 開始,EF 現在生成在許多情況下更有效率的 SQL 程式碼,但仍不支援 SQL Server 2014 和以下版本。
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (
SELECT [n].[value]
FROM OPENJSON(@__names_0) WITH ([value] nvarchar(max) '$') AS [n]
)
請注意,較新的 SQL Server 版本可能會設定為較舊的 相容性層級,也使其與新的 SQL 不相容。 這也可能發生於從先前內部部署 SQL Server 實例移轉的 Azure SQL 資料庫,並承載舊的相容性層級。
原因為何
將常數值插入到 SQL 中會產生許多效能問題,阻礙查詢計劃的快取,並導致其他查詢被不必要的移除。 新的EF Core 8.0 轉譯會使用 SQL Server OPENJSON
函式,改為將值傳送為 JSON 陣列。 這可解決先前技術固有的效能問題:不過,SQL Server 2014 和以下版本無法使用函 OPENJSON
式。
如需這項變更的詳細資訊, 請參閱此部落格文章。
風險降低
如果您的資料庫是 SQL Server 2016 (13.x) 或更新版本,或者如果您使用 Azure SQL,請透過下列命令檢查您資料庫的已設定相容性層級:
SELECT name, compatibility_level FROM sys.databases;
如果相容性層級低於 130 (SQL Server 2016),請考慮將其修改為較新的值(檔)。
否則,如果您的資料庫版本真的比 SQL Server 2016 還舊,或設定為您因某些原因無法變更的舊相容性層級,您可以將 EF 設定為還原至舊版 8.0 之前的 SQL。 如果您使用 EF 9,可以使用新引進的 TranslateParameterizedCollectionsToConstants:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer("<CONNECTION STRING>", o => o.TranslateParameterizedCollectionsToConstants())
如果您使用 EF 8,您可以藉由設定 EF 的 SQL 相容性層級,在使用 SQL Server 時達到相同的效果:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"<CONNECTION STRING>", o => o.UseCompatibilityLevel(120));
LINQ 查詢中 Contains
的可能查詢效能回歸
舊的行為
EF 透過參數化值清單使用 Contains
運算符,對 LINQ 查詢具有特製化支援:
var names = new[] { "Blog1", "Blog2" };
var blogs = await context.Blogs
.Where(b => names.Contains(b.Name))
.ToArrayAsync();
在 EF Core 8.0 之前,EF 會將參數化值插入 SQL 中:
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (N'Blog1', N'Blog2')
新的行為
從 EF Core 8.0 開始,EF 現在會產生下列項目:
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (
SELECT [n].[value]
FROM OPENJSON(@__names_0) WITH ([value] nvarchar(max) '$') AS [n]
)
不過,在 EF 8 發行之後,事實證明,雖然新 SQL 對大多數案例更有效率,但在少數情況下可能會大幅降低效率,甚至在某些情況下造成查詢逾時
風險降低
如果您使用 EF 9,您可以使用新引進的 TranslateParameterizedCollectionsToConstants,將所有查詢的 Contains
轉譯還原回 8.0 之前的行為。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer("<CONNECTION STRING>", o => o.TranslateParameterizedCollectionsToConstants())
如果您使用 EF 8,您可以藉由設定 EF 的 SQL 相容性層級,在使用 SQL Server 時達到相同的效果:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"<CONNECTION STRING>", o => o.UseCompatibilityLevel(120));
最後,您可以使用 EF.Constant 以逐個查詢來控制翻譯,如下所示:
var blogs = await context.Blogs
.Where(b => EF.Constant(names).Contains(b.Name))
.ToArrayAsync();
JSON 中的列舉預設會儲存為 ints,而不是字串
舊的行為
在EF7中, 對應至 JSON 的列舉預設會儲存為 JSON 檔中的字串值。
新的行為
從EF Core 8.0 開始,EF 現在預設會將列舉對應至 JSON 檔中的整數值。
原因為何
根據預設,EF 一律會將列舉對應至關係資料庫中的數值數據行。 因為 EF 支援來自 JSON 的值與資料行和參數值互動的查詢,因此 JSON 中的值必須符合非 JSON 數據行中的值。
風險降低
若要繼續使用字串,請使用轉換來設定列舉屬性。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().Property(e => e.Status).HasConversion<string>();
}
或者,針對列舉類型的所有屬性::
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<StatusEnum>().HaveConversion<string>();
}
中度影響變更
SQL Server date
和 time
現在 Scaffold 至 .NET DateOnly
和 TimeOnly
舊的行為
先前,使用 或 date
數據行建構 SQL Server 資料庫time
時,EF 會產生具有 類型和 DateTime 的TimeSpan實體屬性。
新的行為
從 EF Core 8.0 開始,date
且 time
會以 和 DateOnly的形式建構TimeOnly。
原因為何
DateOnly 和 TimeOnly 是在 .NET 6.0 中引進,而且非常適合用來對應資料庫日期和時間類型。
DateTime 值得注意的是,包含一個未使用的時間元件,而且在對應至 date
時可能會造成混淆,而且 TimeSpan 代表時間間隔,可能包括天,而不是事件發生的一天時間。 使用新類型可防止 Bug 和混淆,並提供意圖的明確性。
風險降低
這項變更只會影響定期將其資料庫重新建構為 EF 程式代碼模型的使用者(“database-first”流程)。
建議您修改程式代碼以使用新 Scaffolded DateOnly 和 TimeOnly 類型,以回應這項變更。 不過,如果不可能,您可以編輯 Scaffolding 範本以還原為先前的對應。 若要這樣做,請設定此頁面所述的範本。 然後,編輯 EntityType.t4
檔案、尋找產生實體屬性的位置(搜尋 property.ClrType
),並將程式代碼變更為下列內容:
var clrType = property.GetColumnType() switch
{
"date" when property.ClrType == typeof(DateOnly) => typeof(DateTime),
"date" when property.ClrType == typeof(DateOnly?) => typeof(DateTime?),
"time" when property.ClrType == typeof(TimeOnly) => typeof(TimeSpan),
"time" when property.ClrType == typeof(TimeOnly?) => typeof(TimeSpan?),
_ => property.ClrType
};
usings.AddRange(code.GetRequiredUsings(clrType));
var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !clrType.IsValueType;
var needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !clrType.IsValueType;
#>
public <#= code.Reference(clrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
具有資料庫產生值的布爾數據行不再以可為 Null 的形式 Scaffold
舊的行為
先前,具有資料庫默認條件約束的非 bool
可為 Null 數據行會建構為可為 bool?
Null 的屬性。
新的行為
從EF Core 8.0 開始,不可為 Null 的數據行一律會建構為不可為 Null bool
的屬性。
原因為何
bool
如果這個值為 ,則屬性不會將其值false
傳送至資料庫,這是CLR預設值。 如果資料庫具有 資料行的預設值,則即使 屬性的值 true
是 false
,資料庫中的值最終還是會是 true
。 不過,在 EF8 中,用來判斷屬性是否有值可變更的 sentinel。 這會自動針對 bool
具有資料庫產生值 true
的屬性執行,這表示不再需要將屬性 Scaffold 設為可為 Null。
風險降低
這項變更只會影響定期將其資料庫重新建構為 EF 程式代碼模型的使用者(“database-first”流程)。
建議您修改程式代碼以使用不可為 Null 的 bool 屬性來回應這項變更。 不過,如果不可能,您可以編輯 Scaffolding 範本以還原為先前的對應。 若要這樣做,請設定此頁面所述的範本。 然後,編輯 EntityType.t4
檔案、尋找產生實體屬性的位置(搜尋 property.ClrType
),並將程式代碼變更為下列內容:
#>
var propertyClrType = property.ClrType != typeof(bool)
|| (property.GetDefaultValueSql() == null && property.GetDefaultValue() != null)
? property.ClrType
: typeof(bool?);
#>
public <#= code.Reference(propertyClrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
<#
低度影響變更
SQLite Math
方法現在會轉譯為 SQL
舊行為
先前只會將 上的 Math
Abs、Max、Min 和 Round 方法轉譯為 SQL。 如果所有其他成員出現在查詢的最終 Select 運算式中,則會在用戶端上進行評估。
新的行為
在 EF Core 8.0 中,具有對應 Math
函式的所有方法都會轉譯為 SQL。
這些數學函式已在我們預設提供的原生 SQLite 連結庫中啟用(透過我們對 SQLitePCLRaw.bundle_e_sqlite3 NuGet 套件的相依性)。 它們也已在SQLitePCLRaw.bundle_e_sqlcipher所提供的連結庫中啟用。 如果您使用其中一個連結庫,您的應用程式不應該受到這項變更的影響。
不過,有一個機會,透過其他方式包含原生 SQLite 連結庫的應用程式可能無法啟用數學函式。 在這些情況下, Math
方法會轉譯為 SQL,並在執行時不會發生 這類函式 錯誤。
原因為何
SQLite 已在 3.35.0 版中新增內建數學函式。 雖然預設會停用它們,但它們已變得很普遍,因此我們決定在 EF Core SQLite 提供者中為它們提供默認翻譯。
我們也與 SQLitePCLRaw 專案上的 Eric Sink 共同作業,以在該專案中提供的所有原生 SQLite 連結庫中啟用數學函式。
風險降低
修正斷點最簡單的方法是盡可能啟用數學函式是原生 SQLite 連結庫,方法是指定 編譯時間選項SQLITE_ENABLE_MATH_FUNCTIONS 。
如果您不控制原生連結庫的編譯,您也可以使用 Microsoft.Data.Sqlite API 在運行時間自行建立函式,藉此修正中斷。
sqliteConnection
.CreateFunction<double, double, double>(
"pow",
Math.Pow,
isDeterministic: true);
或者,您可以將 Select 運算式分割成以 分隔的 AsEnumerable
兩個部分,以強制客戶端評估。
// Before
var query = dbContext.Cylinders
.Select(
c => new
{
Id = c.Id
// May throw "no such function: pow"
Volume = Math.PI * Math.Pow(c.Radius, 2) * c.Height
});
// After
var query = dbContext.Cylinders
// Select the properties you'll need from the database
.Select(
c => new
{
c.Id,
c.Radius,
c.Height
})
// Switch to client-eval
.AsEnumerable()
// Select the final results
.Select(
c => new
{
Id = c.Id,
Volume = Math.PI * Math.Pow(c.Radius, 2) * c.Height
});
ITypeBase 會取代某些 API 中的 IEntityType
舊的行為
先前,所有對應的結構類型都是實體類型。
新的行為
在 EF8 中引進複雜類型時,先前使用的IEntityType
ITypeBase
一些 API,讓 API 可以搭配實體或複雜類型使用。 這包括:
-
IProperty.DeclaringEntityType
現在已過時,IProperty.DeclaringType
應該改用 。 -
IEntityTypeIgnoredConvention
現在已過時,ITypeIgnoredConvention
應該改用 。 -
IValueGeneratorSelector.Select
現在接受ITypeBase
的 ,可能是 ,但不需要是IEntityType
。
原因為何
在 EF8 中引進複雜型別後,這些 API 可以搭配 IEntityType
或 IComplexType
使用。
風險降低
舊的 API 已經過時,但直到 EF10 才會移除。 程式代碼應該更新為使用新的 API ASAP。
ValueConverter 和 ValueComparer 運算式必須針對編譯的模型使用公用 API
舊的行為
先前, ValueConverter
和 ValueComparer
定義未包含在編譯的模型中,因此可以包含任意程序代碼。
新的行為
EF 現在會從 ValueConverter
和 ValueComparer
物件擷取表達式,並在編譯的模型中包含這些 C#。 這表示這些表達式只能使用公用 API。
原因為何
EF 小組會逐漸將更多建構移至已編譯的模型,以支持未來搭配 AOT 使用 EF Core。
風險降低
將比較子使用的 API 設為公用。 例如,請考慮這個簡單的轉換器:
public class MyValueConverter : ValueConverter<string, byte[]>
{
public MyValueConverter()
: base(v => ConvertToBytes(v), v => ConvertToString(v))
{
}
private static string ConvertToString(byte[] bytes)
=> ""; // ... TODO: Conversion code
private static byte[] ConvertToBytes(string chars)
=> Array.Empty<byte>(); // ... TODO: Conversion code
}
若要在編譯的模型中搭配EF8使用此轉換器, ConvertToString
必須將 和 ConvertToBytes
方法公開。 例如:
public class MyValueConverter : ValueConverter<string, byte[]>
{
public MyValueConverter()
: base(v => ConvertToBytes(v), v => ConvertToString(v))
{
}
public static string ConvertToString(byte[] bytes)
=> ""; // ... TODO: Conversion code
public static byte[] ConvertToBytes(string chars)
=> Array.Empty<byte>(); // ... TODO: Conversion code
}
ExcludeFromMigrations 不再排除 TPC 階層中的其他數據表
舊的行為
先前,在 ExcludeFromMigrations
TPC 階層中的數據表上使用 ,也會排除階層中的其他數據表。
新的行為
從 EF Core 8.0 開始, ExcludeFromMigrations
不會影響其他數據表。
原因為何
舊行為是錯誤,並防止移轉用來管理跨專案的階層。
風險降低
ExcludeFromMigrations
明確在應排除的任何其他資料表上使用 。
非陰影整數索引鍵會保存至 Cosmos 檔
舊的行為
先前,符合準則為合成索引鍵屬性的非陰影整數屬性不會保存在 JSON 檔中,而是在出路時重新合成。
新的行為
從EF Core 8.0 開始,這些屬性現在會保存。
原因為何
舊行為是錯誤,並已防止符合合成索引鍵準則的屬性保存至 Cosmos。
風險降低
如果不應該保存屬性的值,請從模型 中排除屬性。
此外,您可以將 AppContext 參數設定 Microsoft.EntityFrameworkCore.Issue31664
為 true
來完全停用此行為,如需詳細資訊,請參閱 連結庫取用者的 AppContext。
AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue31664", isEnabled: true);
關係型模型會在編譯的模型中產生
舊的行為
先前,關係型模型是在運行時間計算,即使使用編譯的模型也一樣。
新的行為
從 EF Core 8.0 開始,關係型模型是所產生編譯模型的一部分。 不過,對於特別大型的模型,產生的檔案可能無法編譯。
原因為何
這樣做是為了進一步改善啟動時間。
風險降低
編輯產生的 *ModelBuilder.cs
檔案,並移除該行 AddRuntimeAnnotation("Relational:RelationalModel", CreateRelationalModel());
以及 方法 CreateRelationalModel()
。
Scaffolding 可能會產生不同的導覽名稱
舊的行為
先前從現有資料庫建構 DbContext
和 實體類型時,關聯性的導覽名稱有時衍生自多個外鍵數據行名稱的通用前置詞。
新的行為
從EF Core 8.0 開始,複合外鍵中數據行名稱的常見前置詞不再用來產生導覽名稱。
原因為何
這是一個模糊的命名規則,有時會產生非常糟糕的名稱,例如、 S
Student_
或甚至只是 _
。 如果沒有此規則,系統就不會再產生奇怪的名稱,而且流覽的命名慣例也會變得更簡單,因此更容易瞭解及預測將產生的名稱。
風險降低
EF Core Power Tools 可以選擇以舊的方式持續產生導覽。 或者,使用 T4 範本可以完全自定義產生的程式代碼。 這可用來範例 Scaffolding 關聯性的外鍵屬性,並使用任何適合您程式代碼的規則來產生您需要的導覽名稱。
歧視性現在具有最大長度
舊的行為
先前,針對 TPH 繼承對應所建立的歧視性數據行是在 SQL Server/Azure SQL 上設定為 nvarchar(max)
,或在其他資料庫上設定對等的未繫結字串類型。
新的行為
從 EF Core 8.0 開始,會建立具有最大長度的歧視性數據行,其長度上限涵蓋所有已知的歧視性值。 EF 會產生移轉以進行這項變更。 不過,如果歧視性數據行以某種方式限制,例如,作為索引的一部分,則 AlterColumn
移轉所建立的 可能會失敗。
原因為何
nvarchar(max)
當已知所有可能值的長度時,數據行沒有效率且不必要。
風險降低
資料行大小可以明確取消系結:
modelBuilder.Entity<Foo>()
.Property<string>("Discriminator")
.HasMaxLength(-1);
SQL Server 索引鍵值不區分大小寫地比較
舊的行為
先前,使用 SQL Server/Azure SQL 資料庫提供者來追蹤具有字串索引鍵的實體時,會使用預設 .NET 區分大小寫的序數比較子來比較索引鍵值。
新的行為
從EF Core 8.0 開始,SQL Server/Azure SQL 字串索引鍵值會使用預設的 .NET 不區分大小寫序數比較子進行比較。
原因為何
根據預設,SQL Server 在比較外鍵值與主體索引鍵值時,會使用不區分大小寫的比較。 這表示當 EF 使用區分大小寫的比較時,它可能不會在應該時將外鍵連接到主體密鑰。
風險降低
藉由設定自定義 ValueComparer
,即可使用區分大小寫的比較。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var comparer = new ValueComparer<string>(
(l, r) => string.Equals(l, r, StringComparison.Ordinal),
v => v.GetHashCode(),
v => v);
modelBuilder.Entity<Blog>()
.Property(e => e.Id)
.Metadata.SetValueComparer(comparer);
modelBuilder.Entity<Post>(
b =>
{
b.Property(e => e.Id).Metadata.SetValueComparer(comparer);
b.Property(e => e.BlogId).Metadata.SetValueComparer(comparer);
});
}
多個 AddDbContext 呼叫會以不同的順序套用
舊的行為
先前,當多次呼叫 AddDbContext
、 AddDbContextPool
或 AddDbContextFactory
AddPooledDbContextFactor
時,使用相同內容類型但發生衝突的組態時,第一個會贏。
新的行為
從 EF Core 8.0 開始,最後一個呼叫的組態優先。
原因為何
這已變更為與可用來在方法之前或之後ConfigureDbContext
新增組態的新方法Add*
一致。
風險降低
反轉呼叫的順序 Add*
。
EntityTypeAttributeConventionBase 取代為 TypeAttributeConventionBase
新的行為
在 EF Core 8.0 EntityTypeAttributeConventionBase
中,已重新命名為 TypeAttributeConventionBase
。
原因為何
TypeAttributeConventionBase
表示功能更好,因為它現在可用於複雜類型和實體類型。
風險降低
將使用量取代 EntityTypeAttributeConventionBase
為 TypeAttributeConventionBase
。