trabajo con tipos de referencia que aceptan valores NULL

C# 8 ha introdujo una nueva característica denominada tipos de referencia que aceptan valores NULL (NRT), lo que permite anotar los tipos de referencia, lo que indica si es válido para que contengan null o no. Si no está familiarizado con esta característica, se recomienda familiarizarse con ella leyendo los documentos de C#. Los tipos de referencia que aceptan valores NULL están habilitados de forma predeterminada en las nuevas plantillas de proyecto, pero permanecen deshabilitados en los proyectos existentes a menos que se opten explícitamente por participar.

En esta página se presenta la compatibilidad de EF Core 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 los tipos de referencia que aceptan valores NULL es la página Propiedades obligatorias y opcionales. Se recomienda empezar leyendo primero esa página.

Nota:

Tenga cuidado al habilitar los tipos de referencia que aceptan valores NULL en un proyecto existente: las propiedades de tipo de referencia que se configuraron previamente como opcionales ahora se configurarán según sea necesario, a menos que se anoten explícitamente para que sean nullables. 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.

Propiedades y inicialización 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 contienen null. Como resultado, no se puede usar la siguiente manera común de escribir tipos de entidad:

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

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

Si usa C# 11 o superior, los miembros necesarios proporcionan la solución perfecta para este problema:

public required string Name { get; set; }

El compilador ahora garantiza que cuando el código crea una instancia de un cliente, siempre inicializa su propiedad Name. Y dado que la columna de base de datos asignada a la propiedad no acepta valores NULL, las instancias cargadas por EF siempre contienen un nombre distinto de NULL.

Si usa una versión anterior de C#, el enlace de constructores es una técnica alternativa 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 constructores no es una opción; Las propiedades de navegación, por ejemplo, no se pueden inicializar de esta manera. En esos casos, simplemente puede inicializar la propiedad a null con la ayuda del operador que admite valores NULL (pero consulte a continuación para obtener más detalles):

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

Propiedades de navegación necesarias

Las propiedades de navegación necesarias presentan una dificultad adicional: aunque siempre existe una dependencia para una entidad de seguridad determinada, puede o no cargarse por una consulta determinada, dependiendo de las necesidades en ese momento del programa (consulte los diferentes patrones para cargar datos). Al mismo tiempo, puede no ser conveniente que estas propiedades sean nullables, ya que esto obligaría a que todos los accesos a ellos comprueben null, incluso cuando se sabe que se carga la navegación y, por tanto, no puede ser null.

¡Esto no es necesariamente un problema! Siempre que se cargue correctamente un dependiente necesario (por ejemplo, a través Include), se garantiza que el acceso a su propiedad de navegación siempre devuelva valores no NULL. Por otro lado, la aplicación puede elegir comprobar si la relación se carga o no comprobando si la navegación es null. En tales casos, es razonable hacer que la navegación acepta valores NULL. Esto significa que las navegaciones necesarias desde el dependiente a la entidad de seguridad:

  • Debe ser no nullable si se considera un error del programador para acceder a una navegación cuando no se carga.
  • Debe ser nullable si es aceptable que el código de aplicación compruebe la navegación para determinar si se carga o no la relación.

Si desea un enfoque más estricto, puede 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));
}

Siempre que la navegación se cargue correctamente, se podrá acceder a la dependencia a través de la propiedad. Sin embargo, si se obtiene acceso a la propiedad sin cargar primero correctamente la entidad relacionada, se produce una excepciónInvalidOperationException, ya que el contrato de API se ha usado incorrectamente.

Nota:

Las navegaciones de colección, que contienen referencias a varias entidades relacionadas, siempre deben ser que 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

Con EF, es habitual tener propiedades DbSet sin inicializar en los tipos de contexto:

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

Aunque esto suele provocar una advertencia del compilador, EF Core 7.0 y versiones posteriores suprimen esta advertencia, ya que EF inicializa automáticamente estas propiedades a través de la reflexión.

En la versión anterior de EF Core, puede solucionar este problema de la siguiente manera:

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

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

Al tratar con relaciones opcionales, es posible encontrar advertencias del compilador en las que una excepción de referencia real null 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 garantía de EF Core y genera advertencias como si la consulta LINQ se ejecutara en memoria, con LINQ to Objects. Como resultado, es necesario usar el operador null-forgiving (!) para informar al compilador de que no es posible un valor real null:

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

Se produce un problema similar al incluir varios niveles de relaciones en las navegaciones opcionales:

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

Si lo hace mucho y los tipos de entidad en cuestión se usan principalmente (o exclusivamente) en las 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 fluida o anotaciones de datos. Esto quitará todas las advertencias del compilador mientras mantiene la relación opcional; sin embargo, si las entidades se recorren fuera de EF Core, puede observar null valores aunque las propiedades se anotan como que no aceptan valores NULL.

Limitaciones en versiones anteriores

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

  • La superficie de LA API pública no se ha anotado por la nulabilidad (la API pública era "null-oblivious"), lo que a veces resulta incómodo usar cuando la característica de NRT está activada. Esto incluye especialmente los operadores LINQ asincrónicos expuestos por EF Core, como FirstOrDefaultAsync. La API pública se anota completamente para la nulabilidad a partir de EF Core 6.0.
  • La ingeniería inversa no admitía tipos de referencia que aceptan valores NULL (NRT)de C# 8: EF Core siempre generó código de C# que presupone que la característica está desactivada. Por ejemplo, se aplicaba scaffolding a las columnas de texto que aceptaban valores NULL como una propiedad con el tipo string, no string?, con la API fluida o las anotaciones de datos usadas para configurar si una propiedad es necesaria o no. Si usa una versión anterior de EF Core, todavía puede editar el código al que se ha aplicado scaffolding y reemplazarlo por anotaciones de nulabilidad de C#.