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

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

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

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

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

Если вы используете более раннюю версию 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, даже если они необходимы.

Это не обязательно проблема! При условии, что требуемый зависимый объект загружен должным образом (например, с помощью Include), доступ к его свойству навигации гарантируется всегда возвращать не null. С другой стороны, доступ к свойству навигации без предварительной загрузки зависимого является ошибкой программиста; Таким образом, свойство навигации может быть приемлемым, чтобы возвращать значение NULL, а когда для кода (buggy) возникает исключение NullReferenceException. В конце концов, код использует EF неправильно.

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

Распространенная практика использования неинициализированных свойств DbSet для типов контекста также является проблематичной, так как компилятор теперь выдает предупреждения для них. Это можно исправить следующим образом:

public class NullableReferenceTypesContext : DbContext
{
    public DbSet<Customer> Customers => Set<Customer>();
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(
                @"Server=(localdb)\mssqllocaldb;Database=EFNullableReferenceTypes;Trusted_Connection=True");
}

Другая стратегия заключается в использовании автосвойств, не допускающих значение NULL, но для их инициализации в значение NULL с помощью оператора, допускающего значение NULL (!), чтобы заставить замолчать предупреждение компилятора. Базовый конструктор DbContext гарантирует, что все свойства DbSet будут инициализированы, и в них никогда не будет наблюдаться значение NULL.

При работе с необязательными связями можно столкнуться с предупреждениями компилятора, когда фактическое исключение пустой ссылки было бы невозможным. При переводе и выполнении запросов LINQ EF Core гарантирует, что если необязательная связанная сущность не существует, любая навигация по ней будет просто игнорироваться, а не создаваться. Однако компилятор не знает об этой гарантии EF Core и выдает предупреждения, как если бы запрос LINQ был выполнен в памяти с LINQ to Objects. В результате необходимо использовать оператор, допускающий значение NULL (!), чтобы сообщить компилятору, что фактическое значение NULL невозможно:

Console.WriteLine(order.OptionalInfo!.ExtraAdditionalInfo!.SomeExtraAdditionalInfo);

Аналогичная проблема возникает при включении нескольких уровней связей между необязательными навигациями:

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