Compartir por


Trabajo con Tipos de Referencia Anulables

Los tipos de referencia anulables de C# permiten anotar los tipos de referencia, indicando si es válido que puedan contener null o no. Si es la primera vez que usa esta función, le recomendamos que se familiarice con ella leyendo la documentación de C#. Los tipos de referencia anulables están activados por defecto en las nuevas plantillas de proyecto, pero permanecen desactivados en los proyectos existentes a menos que se active explícitamente.

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 admitan un valor 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.

Propiedades no anulables e inicialización

Cuando se habilitan los tipos de referencia anulables, el compilador de C# emite advertencias para cualquier propiedad no anulable no inicializada, ya que podría contener 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 la base de datos mapeada a la propiedad no acepta valores nulos, las instancias cargadas por EF siempre contienen un nombre no nulo.

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 existirá un dependiente para un principal dado, puede o no ser cargado por una consulta en particular, dependiendo de las necesidades en ese punto 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 sea nula. Esto significa que las navegaciones necesarias desde el dependiente hacia la entidad principal:

  • ** Debe ser no anulable si se considera un error del programador acceder a un elemento de navegación cuando no está cargado.
  • Debe ser nullable si es aceptable que el código de aplicación verifique la navegación para determinar si la relación está cargada.

Si desea un enfoque más estricto, puede tener una propiedad no anulable con un campo de respaldo que sea anulable:

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 colecciones de navegación, que contienen referencias a varias entidades relacionadas, siempre deben ser no anulables. 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 de DbContext garantiza que todas las propiedades DbSet se inicialicen y que nunca se observe null en ellas.

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 = await context.Orders
    .Where(o => o.OptionalInfo!.SomeProperty == "foo")
    .ToListAsync();

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

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

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

Limitaciones en versiones anteriores

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

  • La interfaz pública de la API no se ha anotado para la nulabilidad (la API pública no tiene en cuenta nulos), lo que a veces es incómodo de usar cuando la característica de Tipos de Referencia Nulos (NRT) está habilitada. Esto incluye especialmente los operadores LINQ asincrónicos expuestos por EF Core, como FirstOrDefaultAsync. La API pública está completamente anotada para la compatibilidad con 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, las columnas de texto anulables se generaron como una propiedad con el tipo string, no string?, utilizando API Fluent o Anotaciones de Datos para configurar si una propiedad es obligatoria o no. Si usa una versión anterior de EF Core, todavía puede editar el código generado automáticamente y reemplazarlo con anotaciones de nulabilidad de C#.