使用可為 Null 的參考型別

C# 8 引進了稱為 可為 Null 參考型別的新功能(NRT), 允許批註參考型別,指出它們是否有效。 null 如果您不熟悉這項功能,建議您閱讀 C# 檔來熟悉此功能。新的專案範本預設會啟用可為 Null 的參考型別,但除非明確加入宣告,否則在現有專案中會保持停用狀態。

此頁面介紹 EF Core 對可為 Null 參考型別的支援,並說明使用它們的最佳作法。

必要和選擇性屬性

必要和選擇性屬性及其與可為 Null 參考型別互動的主要檔是 [必要和選擇性屬性] 頁面。 建議您先閱讀該頁面來開始。

注意

在現有專案上啟用可為 Null 的參考型別時請小心:先前設定為選擇性的參考型別屬性現在會視需要設定,除非它們已明確標注為可為 Null。 管理關係資料庫架構時,這可能會導致產生移轉,以改變資料庫資料行的 Null 性。

不可為 Null 的屬性和初始化

啟用可為 Null 的參考型別時,C# 編譯器會針對任何未初始化的非可為 Null 屬性發出警告,因為這些屬性會包含 null 。 因此,無法使用下列常見的撰寫實體類型方式:

public class Customer
{
    public int Id { get; set; }

    // Generates CS8618, uninitialized non-nullable property:
    public string Name { get; set; }
}

如果您使用 C# 11 或更新版本, 必要成員 會提供此問題的完美解決方案:

public required string Name { get; set; }

編譯器現在會保證當您的程式碼具現化 Customer 時,它一律會初始化其 Name 屬性。 而且,由於對應至 屬性的資料庫資料行不可為 Null,因此 EF 所載入的任何實例一律包含非 Null 名稱。

如果您使用較舊的 C# 版本, 建構函式系結 是一種替代技術,可確保無法為 Null 的屬性初始化:

public class CustomerWithConstructorBinding
{
    public int Id { get; set; }
    public string Name { get; set; }

    public CustomerWithConstructorBinding(string name)
    {
        Name = name;
    }
}

不幸的是,在某些情況下,建構函式系結不是選項;例如,導覽屬性無法以此方式初始化。 在這些情況下,您可以透過 Null 放棄運算子的協助,將 屬性初始化為 null ,但如需詳細資訊,請參閱下方:

public Product Product { get; set; } = null!;

必要的導覽屬性

必要的導覽屬性有額外的困難:雖然給定主體一律會有相依性存在,但特定查詢可能或可能不會載入,視程式當時的需求而定( 請參閱載入資料 的不同模式)。 同時,可能不希望讓這些屬性成為可為 Null,因為這樣會強制所有存取它們進行檢查 null ,即使已知已載入導覽,因此不能 null 為 。

這不一定是個問題! 只要正確載入必要的相依性(例如透過 Include ),存取其導覽屬性一律會傳回非 Null。 另一方面,應用程式可以選擇檢查巡覽 null 是否為 來檢查關聯性是否已載入。 在這種情況下,讓流覽成為可為 Null 是合理的。 這表示需要從 相依至主體的導覽:

  • 如果程式設計人員錯誤在未載入流覽時存取導覽,就應該不可為 Null。
  • 如果可接受應用程式程式碼檢查導覽,以判斷是否載入關聯性,則應該可為 Null。

如果您想要更嚴格的方法,您可以使用可為 Null 的備份欄位 來擁有不可為 Null 的屬性:

private Address? _shippingAddress;

public Address ShippingAddress
{
    set => _shippingAddress = value;
    get => _shippingAddress
           ?? throw new InvalidOperationException("Uninitialized property: " + nameof(ShippingAddress));
}

只要正確載入導覽,就可以透過 屬性存取相依專案。 不過,如果在未先正確載入相關實體的情況下存取 屬性, InvalidOperationException 則會擲回 ,因為 API 合約使用不正確。

注意

集合導覽,其中包含多個相關實體的參考,一律不可為 Null。 空集合表示沒有任何相關的實體存在,但清單本身不應該是 null

DbCoNtext 和 DbSet

使用 EF 時,在內容類型上具有未初始化的 DbSet 屬性是常見的作法:

public class MyContext : DbContext
{
    public DbSet<Customer> Customers { get; set;}
}

雖然這通常會導致編譯器警告,但 EF Core 7.0 和更新版本會隱藏此警告,因為 EF 會自動透過反映初始化這些屬性。

在舊版 EF Core 上,您可以解決此問題,如下所示:

public class MyContext : DbContext
{
    public DbSet<Customer> Customers => Set<Customer>();
}

另一個策略是使用不可為 Null 的自動屬性,但若要將其初始化為 null ,請使用 Null 放棄運算子 (!) 來讓編譯器警告無聲。 DbCoNtext 基底建構函式可確保所有 DbSet 屬性都會初始化,而且永遠不會在它們上觀察到 Null。

處理選擇性關聯性時,可能會遇到編譯器警告,其中實際 null 參考例外狀況是不可能的。 在翻譯和執行 LINQ 查詢時,EF Core 會保證如果選擇性相關實體不存在,則只會忽略任何導覽,而不是擲回。 不過,編譯器並不知道此 EF Core 保證,而且會產生警告,就像在記憶體中執行 LINQ 查詢一樣,使用 LINQ to Objects。 因此,您必須使用 Null 放棄運算子 (!) 來通知編譯器無法取得實際 null 值:

var order = context.Orders
    .Where(o => o.OptionalInfo!.SomeProperty == "foo")
    .ToList();

在選擇性導覽中包含多個層級的關聯性時,就會發生類似的問題:

var order = context.Orders
    .Include(o => o.OptionalInfo!)
    .ThenInclude(op => op.ExtraAdditionalInfo)
    .Single();

如果您發現自己這樣做很多,且有問題的實體類型主要是在 EF Core 查詢中使用,請考慮讓導覽屬性不可為 Null,並透過 Fluent API 或資料批註將其設定為選擇性。 這會移除所有編譯器警告,同時保留關聯性選擇性;不過,如果您的實體是在 EF Core 外部周遊,您可能會觀察到 null 值,不過屬性會標注為不可為 Null。

舊版的限制

在 EF Core 6.0 之前,已套用下列限制:

  • 公用 API 介面並未標注為可為 Null 性(公用 API 為「null-oblivious」),因此在開啟 NRT 功能時有時會很尷尬地使用。 這值得注意的是包含 EF Core 所公開的非同步 LINQ 運算子,例如 FirstOrDefaultAsync 。 從 EF Core 6.0 開始,公用 API 已完全標注為可為 Null。
  • 反向工程不支援 C# 8 可為 Null 的參考型別 (NRTs) :EF Core 一律會產生假設此功能已關閉的 C# 程式碼。 例如,可為 Null 的文字資料行會 Scaffold 為具備類型 string (而非 string?) 的屬性,並使用 Fluent API 或資料註解來設定屬性是否為必要。 如果您使用的是舊版 EF Core,您依然可以編輯 Scaffold 程式碼,並以 C# 可 Null 性註解取代這些程式碼。