Trabajar con tipos de referencia que aceptan valores NULL

C# 8 introdujo una nueva característica denominada tipos de referencia que aceptan valores NULL (NRT),que permite anotar tipos de referencia, lo que indica si es válido que contengan null o no. Si no está familiarizado con esta característica, se recomienda familiarizarse con ella mediante la lectura de los documentos de C#.

En esta página se EF Core compatibilidad con tipos de referencia que aceptan valores NULL y se describen los procedimientos recomendados para trabajar con ellos.

Propiedades obligatorias y opcionales

La documentación principal sobre las propiedades obligatorias y opcionales y su interacción con tipos de referencia que aceptan valores NULL es la página Propiedades obligatorias y opcionales. Se recomienda empezar por leer primero esa página.

Nota

Tenga cuidado al habilitar tipos de referencia que aceptan valores NULL en un proyecto existente: las propiedades de tipo de referencia que se configuraron anteriormente como opcionales ahora se configurarán como necesarias, a menos que se anoten explícitamente para que sean que aceptan valores NULL. Al administrar un esquema de base de datos relacional, esto puede provocar que se generen migraciones que modifiquen la nulabilidad de la columna de base de datos.

Inicialización y propiedades que no aceptan valores NULL

Cuando se habilitan los tipos de referencia que aceptan valores NULL, el compilador de C# emite advertencias para cualquier propiedad que no acepta valores NULL sin inicializar, ya que estas contienen valores NULL. Como resultado, no se puede usar la siguiente manera común de escribir tipos de entidad:

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

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

El enlace de constructor es una técnica útil para asegurarse de que se inicializan las propiedades que no aceptan valores NULL:

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

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

Desafortunadamente, en algunos escenarios, el enlace de constructor no es una opción; Las propiedades de navegación, por ejemplo, no se pueden inicializar de esta manera.

Las propiedades de navegación requeridas presentan una dificultad adicional: aunque un elemento dependiente siempre existirá para una entidad de seguridad determinada, una consulta determinada puede cargarla o no, en función de las necesidades en ese momento del programa (vea los diferentes patrones para cargardatos). Al mismo tiempo, no es deseable que estas propiedades aceptan valores NULL, ya que eso obligaría a todos los accesos a ellas a comprobar si son NULL, incluso si son necesarias.

Una manera de tratar estos escenarios es tener una propiedad que no acepta valores NULL con un campo de respaldo que acepta valores NULL:

private Address? _shippingAddress;

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

Puesto que la propiedad de navegación no acepta valores NULL, se configura una navegación necesaria; y siempre que la navegación se cargue correctamente, el dependiente será accesible a través de la propiedad . Sin embargo, si se tiene acceso a la propiedad sin cargar primero correctamente la entidad relacionada, se produce una excepción InvalidOperationException, ya que el contrato de API se ha usado incorrectamente. Tenga en cuenta que EF debe configurarse para tener acceso siempre al campo de respaldo y no a la propiedad , ya que se basa en poder leer el valor incluso cuando se desasoye. Consulte la documentación sobre los campos de respaldo sobre cómo hacerlo y considere la posibilidad de especificar para asegurarse de que la configuración es correcta.

Como alternativa a terser, es posible simplemente inicializar la propiedad en NULL con la ayuda del operador que permite valores NULL (!):

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

Nunca se observará un valor NULL real excepto como resultado de un error de programación, por ejemplo, al acceder a la propiedad de navegación sin cargar correctamente la entidad relacionada de antemano.

Nota

Las navegacións de colección, que contienen referencias a varias entidades relacionadas, siempre deben no aceptan valores NULL. Una colección vacía significa que no existen entidades relacionadas, pero la propia lista nunca debe ser NULL.

DbContext y DbSet

La práctica común de tener propiedades DbSet no inicializadas en tipos de contexto también es problemática, ya que el compilador emitirá ahora advertencias para ellos. Esto se puede solucionar de la siguiente manera:

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");
}

Otra estrategia es usar propiedades automáticas que no aceptan valores NULL, pero inicializarlos en NULL, mediante el operador que permite valores NULL (!) para silenciar la advertencia del compilador. El constructor base DbContext garantiza que se inicializarán todas las propiedades de DbSet y que nunca se observará null en ellas.

Cuando se trabaja con relaciones opcionales, es posible encontrar advertencias del compilador en las que una excepción de referencia nula real sería imposible. Al traducir y ejecutar las consultas LINQ, EF Core garantiza que, si no existe una entidad relacionada opcional, cualquier navegación a ella simplemente se omitirá, en lugar de iniciarse. Sin embargo, el compilador no es consciente de esta EF Core garantía y genera advertencias como si la consulta LINQ se ejecutara en memoria, con LINQ to Objects. Como resultado, es necesario usar el operador que permite valores NULL (!) para informar al compilador de que no es posible un valor NULL real:

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

Se produce un problema similar al incluir varios niveles de relaciones entre navegación opcionales:

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

Si se encuentra haciendo esto mucho y los tipos de entidad en cuestión se usan principalmente (o exclusivamente) en consultas de EF Core, considere la posibilidad de hacer que las propiedades de navegación no aceptan valores NULL y configurarlas como opcionales a través de la API de Fluent o anotaciones de datos. Esto quitará todas las advertencias del compilador mientras se mantiene la relación opcional; sin embargo, si las entidades se atraviesan fuera de EF Core, puede observar valores NULL aunque las propiedades se anotan como no acepta valores NULL.

Limitaciones en versiones anteriores

Antes de EF Core 6.0, se aplicaba las siguientes limitaciones:

  • La superficie de API pública no se anotó para la nulabilidad (la API pública era "sin valores NULL"), lo que a veces resulta difícil de usar cuando se ha activado la característica NRT. Esto incluye en particular los operadores LINQ asincrónicos expuestos por EF Core, como FirstOrDefaultAsync. La API pública se anota completamente para la nulabilidad a partir EF Core 6.0.
  • La ingeniería inversa no admite tipos de referencia que aceptan valores NULL (NRT) de C# 8:EF Core código de C# generado siempre que se supone que la característica está desactivada. Por ejemplo, las columnas de texto que aceptan valores NULL se han scaffolding como una propiedad con el tipo , no , con la API de Fluent o anotaciones de datos utilizadas para configurar si una propiedad es necesaria o stringstring? no. Si usa una versión anterior de EF Core, todavía puede editar el código con scaffolding y reemplazarlos por anotaciones de nulabilidad de C#.