Null 許容参照型の使用

C# 8 では、Null 許容参照型 (NRT) と呼ばれる新機能が導入され、参照型の値として null を含むことが有効かどうかを示す注釈を設定できるようになりました。 この機能を初めて使用する場合は、C# のドキュメントを参照して理解を深めてください。Null 許容参照型は、新しいプロジェクト テンプレートでは既定で有効になっていますが、明示的に選択しない限り、既存のプロジェクトでは無効のままです。

このページでは、Null 許容参照型に対する EF Core のサポートについて説明し、これを使用するためのベスト プラクティスについて説明します。

必須および省略可能なプロパティ

必須および省略可能なプロパティと、Null 許容参照型とそれらの相互作用に関する主なドキュメントについては、「必須のプロパティと省略可能なプロパティ」のページを参照してください。 最初にこのページを読むことをお勧めします。

Note

既存のプロジェクトで 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 以降を使用している場合は、required メンバーがこの問題に対する完全な解決策になります。

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 コントラクトが正しく使用されなかったためです。

Note

複数の関連エンティティへの参照を含むコレクション ナビゲーションは、常に 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>();
}

もう 1 つの方法として、null 非許容の自動プロパティを使用しますが、それらを null に初期化するために、null 免除演算子 (!) を使用してコンパイラの警告をサイレント状態にします。 DbContext 基本コンストラクターによって、すべての DbSet プロパティが初期化され、それらについて Null が検出されることはありません。

省略可能なリレーションシップを処理するとき、実際の null 参照例外が不可能な場合にコンパイラの警告が出される可能性があります。 LINQ クエリの変換と実行を行うとき、EF Core では、オプションの関連エンティティが存在しない場合は、その関連エンティティへのナビゲーションが単純に無視され、スローされないことが保証されます。 ただし、コンパイラではこの EF Core の保証が認識されないため、LINQ to Objects を使用して、LINQ クエリがメモリ内で実行された場合と同様に警告が生成されます。 結果として、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 許容テキスト列は、string? ではなく string 型のプロパティとしてスキャフォールディングされ、プロパティが必須かどうかを構成するために Fluent API またはデータ注釈のいずれかが使用されました。 以前のバージョンの EF Core を使う場合でも、スキャフォールディングされたコードを編集し、これらを C# Null 許容の注釈に置き換えることができます。