Работа со ссылочными типами, допускающими значение NULL

В C# 8 появилась новая функция, называемая ссылочными типами, допускающими значение NULL( NRT), что позволяет типы ссылок быть аннотированы, указывая, является ли он допустимым для них содержать null или нет. Если вы не знакомы с этой функцией, рекомендуется ознакомиться с ней, прочитав документы C#. Ссылочные типы, допускающие значение NULL, включены по умолчанию в новых шаблонах проектов, но остаются отключенными в существующих проектах, если явно не будет разрешено.

На этой странице представлена поддержка EF Core для ссылочных типов, допускающих значение NULL, и описаны рекомендации по работе с ними.

Обязательные и необязательные свойства

Основная документация по обязательным и необязательным свойствам и их взаимодействию с типами ссылок, допускающих значение NULL, — это страница "Обязательные и необязательные свойства ". Сначала рекомендуется начать чтение этой страницы.

Примечание.

Соблюдайте осторожность при включении ссылочных типов, допускающих значение NULL в существующем проекте: свойства ссылочного типа, которые ранее были настроены как необязательные, теперь будут настроены в соответствии с обязательными требованиями, если они явно не помечены как значения NULL. При управлении схемой реляционной базы данных это может привести к созданию миграций, которые изменяют допустимость null столбца базы данных.

Непустые свойства и инициализация

Если включены ссылочные типы, допускающие значение NULL, компилятор C# выдает предупреждения для любого неинициализированного ненулевого свойства, так как они будут содержать 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, всегда содержат ненулевое имя.

Если вы используете более раннюю версию C#, привязка конструктора является альтернативным способом, чтобы гарантировать, что ненулевое свойство инициализировано:

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.

Это не обязательно проблема! Если требуемая зависимость правильно загружена (например, через Include), доступ к свойству навигации гарантируется всегда возвращать ненулевое значение. С другой стороны, приложение может выбрать проверка, загружается ли связь, проверка при наличии навигации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-forgiving (!) для информирования компилятора о том, что фактическое 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, а также настраивать их как необязательные через API Fluent или заметки данных. Это приведет к удалению всех предупреждений компилятора при сохранении связи необязательным; Однако если сущности пересекаются за пределами EF Core, могут наблюдаться null значения, хотя свойства аннотируются как не допускающие значения NULL.

Ограничения в более ранних версиях

До EF Core 6.0 применяются следующие ограничения:

  • Область общедоступного API не была помечена как ненужная (общедоступный API был "неумышленным"), что иногда неловко использовать, когда функция NRT включена. Это особенно включает асинхронные операторы LINQ, предоставляемые EF Core, например FirstOrDefaultAsync. Общедоступный API полностью аннотирован для nullability, начиная с EF Core 6.0.
  • Обратная инженерия не поддерживает ссылочные типы ссылок на C# 8, допускающие значение NULL (NRTs): EF Core всегда создал код C#, предполагающий, что эта функция отключена. Например, шаблон текстовых столбцов, допускающих значение NULL, формировался как свойство с типом string, а не string?. При этом использовались текучий API или аннотации к данным, чтобы определить, является ли свойство обязательным. Если вы используете более раннюю версию EF Core, вы по-прежнему можете изменить сформированный шаблон кода и заменить его заметками о допустимости значений NULL в C#.