共用方式為


使用可為 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));
}

只要正確載入導覽,您就可以透過屬性存取相依項目。 不過,如果沒有先正確載入相關實體就存取屬性,由於 API 合約使用不正確,系統會擲回 InvalidOperationException

注意

包含多個相關實體參照的集合導覽,應一律是非可為 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」),因此在開啟 NRT 功能時,使用起來不太順暢。 值得注意的是,這也包含 EF Core 公開的非同步 LINQ 運算子,例如 FirstOrDefaultAsync。 從 EF Core 6.0 開始,公用 API 會完整標註可 Null 性。
  • 反向工程不支援 C# 8 的可為 Null 參照類型 (NRT):EF Core 產生的 C# 程式碼一律假設此功能已關閉。 例如,可為 Null 的文字資料行會 Scaffold 為具備類型 string (而非 string?) 的屬性,並使用 Fluent API 或資料註解來設定屬性是否為必要。 如果您使用的是舊版 EF Core,您依然可以編輯 Scaffold 程式碼,並以 C# 可 Null 性註解取代這些程式碼。