Trabalhando com tipos de referência que permitem valor nulo

O C# 8 apresentou um novo recurso chamado NRT (tipos de referência que permitem valor nulo), permitindo que os tipos de referência sejam anotados, indicando se é válido que eles contenham null ou não. Se você ainda não conhece esse recurso, é recomendável que você leia os documentos do C# para se familiarizar. Os tipos de referência que permitem valor nulo são habilitados por padrão em novos modelos de projeto, mas permanecem desabilitados em projetos existentes, a menos que seu uso seja explicitamente aceito.

Esta página apresenta o suporte do EF Core para tipos de referência que permitem valor nulo e descreve as melhores práticas para trabalhar com eles.

Propriedades obrigatórias e opcionais

A documentação principal sobre as propriedades obrigatórias e opcionais e sua interação com tipos de referência que permitem valor nulo é a página Propriedades Obrigatórias e Opcionais. É recomendável que você comece lendo essa página.

Observação

Tenha cuidado ao habilitar tipos de referência que permitem valor nulo em um projeto existente: as propriedades de tipo de referência que foram configuradas anteriormente como opcionais agora serão configuradas conforme necessário, a menos que seja explicitamente anotado que permitem valor nulo. Ao gerenciar um esquema de banco de dados relacional, isso pode fazer com que as migrações sejam geradas, alterando a nulidade da coluna de banco de dados.

Propriedades e inicialização não anuláveis

Quando tipos de referência que permitem valor nulo são habilitados, o compilador C# emite avisos para qualquer propriedade não anulável não inicializada, pois elas contêm null. Como resultado, a seguinte maneira comum de gravar tipos de entidade não pode ser usada:

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

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

Se você estiver usando o C# 11 ou superior, os membros necessários fornecem a solução perfeita para esse problema:

public required string Name { get; set; }

O compilador agora garante que, quando seu código cria uma instância de um Cliente, ele sempre inicializa a propriedade Name. E como a coluna de banco de dados mapeada para a propriedade não é anulável, todas as instâncias carregadas pelo EF sempre contêm um Nome diferente de nulo.

Se você estiver usando uma versão mais antiga do C#, a Associação de construtor será uma técnica alternativa para garantir que suas propriedades não anuláveis sejam inicializadas:

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

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

Infelizmente, em alguns cenários, a associação de construtor não é possível. As propriedades de navegação, por exemplo, não podem ser inicializadas dessa maneira. Nesses casos, você pode inicializar a propriedade para null com a ajuda do operador tolerante a nulo (veja abaixo para obter mais detalhes):

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

Propriedades de navegação obrigatórias

As propriedades de navegação obrigatórias apresentam uma dificuldade adicional: embora um dependente sempre exista para uma determinada entidade de segurança, ele pode ou não ser carregado por uma consulta específica, dependendo das necessidades nesse ponto do programa (veja os diferentes padrões para carregamento de dados). Ao mesmo tempo, pode não ser desejável tornar essas propriedades anuláveis, pois isso forçaria todo o acesso a elas a verificar null, mesmo quando a navegação é conhecida por ser carregada e, portanto, não pode ser null.

Isso não é necessariamente um problema! Desde que um dependente obrigatório seja carregado corretamente (por exemplo, via Include), o acesso à propriedade de navegação é garantido para sempre retornar não nulo. Por outro lado, o aplicativo pode optar por verificar se a relação está carregada ou não verificando se a navegação está null. Nesses casos, é razoável tornar a navegação anulável. Isso significa que as navegações obrigatórias do dependente para a entidade de segurança:

  • Deve ser não anulável se for considerado um erro do programador acessar uma navegação quando ela não é carregada.
  • Deve ser anulável se for aceitável que o código do aplicativo verifique a navegação para determinar se a relação está carregada ou não.

Para uma abordagem mais rigorosa, você pode ter uma propriedade não anulável com um campo de suporte anulável:

private Address? _shippingAddress;

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

Desde que a navegação seja carregada corretamente, o dependente estará acessível por meio da propriedade. Se, no entanto, a propriedade for acessada sem primeiro carregar corretamente a entidade relacionada, InvalidOperationException será gerado, já que o contrato de API foi usado incorretamente.

Observação

As navegações de coleção, que contêm referências a várias entidades relacionadas, sempre devem ser não anuláveis. Uma coleção vazia significa que nenhuma entidade relacionada existe, mas a própria lista nunca deve ser null.

DbContext e DbSet

Com o EF, é uma prática comum ter propriedades DbSet não inicializadas em tipos de contexto:

public class MyContext : DbContext
{
    public DbSet<Customer> Customers { get; set;}
}

Embora isso geralmente cause um aviso do compilador, o EF Core 7.0 e superior suprime esse aviso, já que o EF inicializa automaticamente essas propriedades por meio de reflexão.

Na versão mais antiga do EF Core, você pode contornar esse problema da seguinte maneira:

public class MyContext : DbContext
{
    public DbSet<Customer> Customers => Set<Customer>();
}

Outra estratégia é usar propriedades automáticas não anuláveis, mas inicializá-las para null, usando o operador tolerante a nulo (!) para silenciar o aviso do compilador. O construtor de base DbContext garante que todas as propriedades DbSet sejam inicializadas e que nulos nunca sejam observados nelas.

Ao lidar com relações opcionais, é possível encontrar avisos do compilador em que uma exceção de referência real de null seria impossível. Ao traduzir e executar consultas LINQ, o EF Core garante que, se uma entidade relacionada opcional não existir, qualquer navegação nela será ignorada ao invés de gerada. No entanto, o compilador não está ciente dessa garantia do EF Core e produz avisos como se a consulta LINQ tivesse sido executada na memória, com LINQ to Objects. Como resultado, é necessário usar o operador tolerante a nulo (!) para informar ao compilador que um valor de null real não é possível:

var order = context.Orders
    .Where(o => o.OptionalInfo!.SomeProperty == "foo")
    .ToList();

Um problema semelhante ocorre ao incluir vários níveis de relações entre navegações opcionais:

var order = context.Orders
    .Include(o => o.OptionalInfo!)
    .ThenInclude(op => op.ExtraAdditionalInfo)
    .Single();

Se você fizer isso com frequência e os tipos de entidade em questão forem predominantemente (ou exclusivamente) usados em consultas do EF Core, considere tornar as propriedades de navegação não anuláveis e configurá-las como opcionais por meio da API fluente ou Anotações de Dados. Isso removerá todos os avisos do compilador mantendo a relação opcional. No entanto, se as entidades forem percorridas fora do EF Core, você poderá observar valores de null, embora as propriedades sejam anotadas como não anuláveis.

Limitações em versões mais antigas

Antes do EF Core 6.0, as seguintes limitações se aplicavam:

  • A superfície da API pública não foi anotada para nulidade (a API pública era "nula alheia") e seu uso pode causar estranheza quando o recurso NRT é ativado. Isso inclui os operadores LINQ assíncronos expostos pelo EF Core, como FirstOrDefaultAsync. A API pública é totalmente anotada para nulidade a partir do EF Core 6.0.
  • A engenharia reversa não deu suporte a NRTs (tipos de referência que permitem valor nulo) do C# 8: o EF Core sempre gerava código C# que presumia que o recurso estava desativado. Por exemplo, colunas de texto anuláveis foram scaffolded como uma propriedade com tipo string, não string?, com a API Fluent ou Anotações de Dados usadas para configurar se uma propriedade é necessária ou não. Se estiver usando uma versão mais antiga do EF Core, você ainda poderá editar o código scaffolded e substituí-lo por anotações de nulidade em C#.