今天下午在排查一個EF問題時,遇到了個很隱蔽的坑,特此記錄。
問題
使用ef執行Insert物件到某表時報錯,此物件的Address為空:
不能將值 NULL 插入列 'Address',表 'dbo.xxx';列不允許有 Null 值。INSERT 失敗。
檢查資料庫和遷移檔案時發現Address這個欄位被意外設定成nullable: false
,而其它的欄位卻正常,按理來說對於string型別的屬性,EFCore在codefirst模式下應該對映為可空型別。
程式碼也確認了實體中不包含[Required]註釋,在任何地方也沒有出現.IsRequired()的呼叫。
於是開始排查:手動建立一個空程式集,引用EFCore,從原專案複製EF設計時庫、DbContext和各實體類,一頓操作後竟然發現在新的程式集中生成的遷移檔案是符合預期的。
令人費解,在多次比對程式碼之後,發現是.csproj
檔案中的這一行配置導致的
<Nullable>enable</Nullable>
原因分析
C# 8 引入了一項名為可為 null 引用型別 (NRT) 的新功能。官方文件
該功能允許對引用型別進行批註,指示引用型別能否包含 null。
透過檢視EF文件瞭解到,可為空引用型別透過以下方式影響 EF Core 的行為:
- 如果禁用可為空引用型別,則按約定將具有 .NET 引用型別的所有屬性配置為可選 (例如 string ) 。
- 如果啟用了可為 null 的引用型別,則基於屬性的 .NET 型別的 C# 為 Null 性來配置屬性:string? 將配置為可選屬性,但 string 將配置為必需屬性。
換而言之,啟用了該功能後,把原本《引用型別可為空》的這個傳統約定,更改稱為了《引用型別是否可為空,是透過?
語法來表明的》,實體中string型別的屬性在C#中作為引用型別,自然而然地受到了這個影響。
果然,在刪除了這個功能後,string?
的語法將不起作用
解決
關閉此功能,重新生成遷移,更新資料庫,問題解決。
後記
語言特性會影響EF實體與表結構對映的約定,官方示例中對於string型別的處理方式也做了說明:
無NRT
public class CustomerWithoutNullableReferenceTypes
{
public int Id { get; set; }
[Required] // Data annotations needed to configure as required
public string FirstName { get; set; }
[Required]
public string LastName { get; set; } // Data annotations needed to configure as required
public string MiddleName { get; set; } // Optional by convention
}
有NRT
public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; } // Required by convention
public string LastName { get; set; } // Required by convention
public string? MiddleName { get; set; } // Optional by convention
// Note the following use of constructor binding, which avoids compiled warnings
// for uninitialized non-nullable properties.
public Customer(string firstName, string lastName, string? middleName = null)
{
FirstName = firstName;
LastName = lastName;
MiddleName = middleName;
}
}
這兩種模型的資料庫對映是等價的。
之後應留意專案的"NRT"功能是否開啟,在解決方案.csproj
檔案中用如下方式關閉
<Nullable>disable</Nullable>
留意實體類中是否有程式碼段被標識"NRT"功能開啟
#nullable disable
#nullable enable
從 .NET 6 開始,預設情況下會為新專案啟用這些功能。原始專案是.NET 5.0升級而來的,所以專案檔案中並不會包含Nullable相關的配置。
為了一行bug,好值得的一個下午呢