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

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

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

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

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

Примечание

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

Свойства и инициализация, не допускающая значения NULL

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

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

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

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

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

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

К сожалению, в некоторых сценариях привязка конструктора не является вариантом; Например, свойства навигации не могут быть инициализированы таким образом.

Обязательные свойства навигации представляют дополнительную сложность: Несмотря на то, что зависимый объект всегда будет существовать для данного участника, он может быть или не загружен определенным запросом в зависимости от потребностей в этой точке программы (см.различные шаблоны для загрузки данных). В то же время нежелательно делать эти свойства допускающими значение null, так как это приведет к тому, что все права доступа к ним будут проверять наличие значения NULL, даже если они требуются.

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

private Address? _shippingAddress;

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

Так как свойство навигации не допускает значения NULL, необходимо настроить требуемую навигацию. и при условии, что Навигация правильно загружена, зависимый объект будет доступен через свойство. Однако если доступ к свойству осуществляется без предварительной загрузки связанной сущности, создается исключение InvalidOperationException, так как контракт API используется неправильно. Обратите внимание, что EF должен быть настроен так, чтобы всегда обращаться к резервному полю, а не к свойству, так как оно зависит от возможности чтения значения даже при неопределенном значении. сведения о том, как это сделать, см. в документации по резервным полям , а также о PropertyAccessMode.Field том, чтобы убедиться, что конфигурация правильная.

В качестве более сжатого альтернативного варианта можно просто инициализировать свойство значением NULL с помощью оператора NULL-терпим отношению (!):

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

Фактическое значение NULL не будет наблюдаться, за исключением случая программной ошибки, например при доступе к свойству навигации без правильной загрузки связанной сущности заранее.

Примечание

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

Ограничения

  • В настоящее время реконструирование не поддерживает Ссылочные типы C# 8 Nullable (НРТС): EF Core всегда создает код c#, который предполагает, что эта функция отключена. Например, столбцы с текстом, допускающими значение null, будут формироваться в виде свойства с типом string , а не string? с помощью API-интерфейса Fluent или заметок к данным, используемых для настройки того, является ли свойство обязательным. Вы можете изменить сформированный код и заменить их на метки допустимости значений NULL в C#. Поддержка формирования шаблонов для ссылочных типов, допускающих значения NULL, ведется по выпуску #15520.
  • До EF Core 6,0, в области открытого API не было указано допустимость значений NULL (общедоступный API был "null-очевидным"), что иногда может оказаться неудобным для использования, когда включена функция ПРЕВЕНТИВНОЙ. Это особенно относится к асинхронным операторам LINQ, предоставляемым EF Core, таким как фирстордефаултасинк. Общедоступный API полностью снабжен заметками о допустимости значений NULL, начиная с EF Core 6,0.