Share via


Navegaciones de relaciones

Las relaciones de EF Core se definen mediante claves externas. Las navegaciones se superponen sobre claves externas a fin de proporcionar una vista natural orientada a objetos para leer y manipular relaciones. Mediante las navegaciones, las aplicaciones pueden trabajar con gráficos de entidades sin preocuparse de lo que sucede con los valores de claves externas.

Importante

Varias relaciones no pueden compartir navegaciones. Cualquier clave externa se puede asociar como máximo a una navegación de entidad de seguridad a dependiente y, como máximo, una navegación de dependiente a entidad de seguridad.

Sugerencia

No es necesario hacer que las navegaciones sean virtuales a menos que las utilicen la carga diferida o los servidores proxy de seguimiento de cambios.

Navegaciones de referencia

Las navegaciones se presentan en dos formas: referencia y colección. Las navegaciones de referencia son referencias a objetos simples a otra entidad. Representan el lado o lados "uno" de las relaciones uno a varios y uno a uno. Por ejemplo:

public Blog TheBlog { get; set; }

Las navegaciones de referencia deben tener un establecedor, aunque no es necesario que sea público. Las navegaciones de referencia no se deben inicializar automáticamente en un valor predeterminado distinto de NULL; hacerlo equivale a afirmar que existe una entidad cuando no es así.

Cuando se utilizan tipos de referencia que admiten un valor NULL en C#, las navegaciones de referencia deben admitir un valor NULL para las relaciones opcionales:

public Blog? TheBlog { get; set; }

Las navegaciones de referencia para las relaciones necesarias pueden admitir un valor NULL o pueden no aceptar valores NULL.

Navegaciones de colección

Las navegaciones de colección son instancias de un tipo de colección de .NET; es decir, cualquier tipo que implemente ICollection<T>. La colección contiene instancias del tipo de entidad relacionada, de las que puede haber cualquier número. Representan el lado o lados "varios" de las relaciones uno a varios y varios a varios. Por ejemplo:

public ICollection<Post> ThePosts { get; set; }

Las navegaciones de colección no necesitan tener un establecedor. Es habitual inicializar la colección en línea, lo que elimina la necesidad de comprobar siempre si la propiedad es null. Por ejemplo:

public ICollection<Post> ThePosts { get; } = new List<Post>();

Sugerencia

No debe crear accidentalmente una propiedad con forma de expresión, como, por ejemplo, public ICollection<Post> ThePosts => new List<Post>();. Esto creará una nueva instancia de colección vacía cada vez que se acceda a la propiedad y, por lo tanto, no tendrá utilidad como navegación.

Tipos de colección

La instancia de colección subyacente debe implementar ICollection<T> y debe tener un método de trabajo Add. Es habitual utilizar List<T> o HashSet<T>. List<T> es eficaz para un número reducido de entidades relacionadas y mantiene un orden estable. HashSet<T> cuenta con unas búsquedas más eficaces para un gran número de entidades, pero no tiene un orden estable. También puede usar su propia implementación de colección personalizada.

Importante

La colección debe utilizar la igualdad de referencia. Al crear HashSet<T> para una navegación de colección, asegúrese de usar ReferenceEqualityComparer.

No se pueden utilizar matrices para las navegaciones de colección porque, aunque implementan ICollection<T>, el método Add produce una excepción cuando se le llama.

Aunque la instancia de colección debe ser ICollection<T>, la colección no necesita exponerse como tal. Por ejemplo, es habitual exponer la navegación como IEnumerable<T>, que proporciona una vista de solo lectura que no se puede modificar aleatoriamente mediante código de aplicación. Por ejemplo:

public class Blog
{
    public int Id { get; set; }
    public IEnumerable<Post> ThePosts { get; } = new List<Post>();
}

Una variación de este patrón incluye métodos para la manipulación de la colección según sea necesario. Por ejemplo:

public class Blog
{
    private readonly List<Post> _posts = new();

    public int Id { get; set; }

    public IEnumerable<Post> Posts => _posts;

    public void AddPost(Post post) => _posts.Add(post);
}

El código de aplicación todavía podría convertir la colección expuesta en ICollection<T> y luego manipularla. Si esto es un problema, la entidad podría devolver una copia defensiva de la colección. Por ejemplo:

public class Blog
{
    private readonly List<Post> _posts = new();

    public int Id { get; set; }

    public IEnumerable<Post> Posts => _posts.ToList();

    public void AddPost(Post post) => _posts.Add(post);
}

Considere con cuidado si el valor obtenido con ello es lo suficientemente alto como para compensar la sobrecarga que supone crear una copia de la colección cada vez que se accede a la navegación.

Sugerencia

Este patrón final funciona porque, de forma predeterminada, EF accede a la colección mediante su campo de respaldo. Esto significa que EF agrega y quita entidades de la colección real, mientras que las aplicaciones solo interactúan con una copia defensiva de la colección.

Inicialización de las navegaciones de colección

Las navegaciones de colección se pueden inicializar mediante el tipo de entidad, ya sea mediante una carga diligente:

public class Blog
{
    public ICollection<Post> Posts { get; } = new List<Post>();
}

O una carga diferida:

public class Blog
{
    private ICollection<Post>? _posts;

    public ICollection<Post> Posts => _posts ??= new List<Post>();
}

Si EF necesita agregar una entidad a una navegación de colección, por ejemplo, mientras ejecuta una consulta, entonces inicializará la colección si actualmente es null. La instancia creada depende del tipo expuesto de la navegación.

  • Si la navegación se expone como HashSet<T>, se crea una instancia de HashSet<T> mediante ReferenceEqualityComparer.
  • De lo contrario, si la navegación se expone como un tipo concreto con un constructor sin parámetros, se crea una instancia de ese tipo concreto. Esto se aplica a List<T>, pero también a otros tipos de colección, incluidos los tipos de colección personalizados.
  • De lo contrario, si la navegación se expone como IEnumerable<T>, ICollection<T> o ISet<T>, se crea una instancia de HashSet<T> mediante ReferenceEqualityComparer.
  • De lo contrario, si la navegación se expone como IList<T>, se crea una instancia de List<T>.
  • De lo contrario, se inicia una excepción.

Nota

Si se usan entidades de notificación, incluidos servidores proxy de seguimiento de cambios, entonces se utilizan ObservableCollection<T> y ObservableHashSet<T> en lugar de List<T> y HashSet<T>.

Importante

Como se describe en la documentación de seguimiento de cambios, EF solo realiza un seguimiento de una sola instancia de cualquier entidad con un valor de clave determinado. Esto significa que las colecciones usadas como navegaciones deben utilizar la semántica de igualdad de referencia. Los tipos de entidad que no invalidan la igualdad de objetos la obtendrán de forma predeterminada. Asegúrese de usar ReferenceEqualityComparer al crear HashSet<T> a fin de utilizarla como navegación para asegurarse de que funciona para todos los tipos de entidad.

Configuración de las navegaciones

Las navegaciones se incluyen en el modelo como parte de la configuración de una relación. Es decir, por convención o mediante HasOne, HasMany, etc. en la API de compilación de modelos. La mayor parte de la configuración asociada a las navegaciones se realiza configurando la propia relación.

Sin embargo, hay algunos tipos de configuración que son específicos de las propiedades de navegación en sí, en lugar de formar parte de la configuración general de la relación. Este tipo de configuración se realiza con el método Navigation. Por ejemplo, para forzar a EF a tener acceso a la navegación mediante su propiedad, en lugar de usar el campo de respaldo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Navigation(e => e.Posts)
        .UsePropertyAccessMode(PropertyAccessMode.Property);

    modelBuilder.Entity<Post>()
        .Navigation(e => e.Blog)
        .UsePropertyAccessMode(PropertyAccessMode.Property);
}

Nota

No se puede utilizar la llamada Navigation para crear una propiedad de navegación. Solo se usa para configurar una propiedad de navegación que se haya creado anteriormente mediante la definición de una relación o desde una convención.

Navegaciones necesarias

Se requiere una navegación de dependiente a entidad de seguridad si la relación es necesaria, lo que significa a su vez que la propiedad de clave externa no acepta valores NULL. Por el contrario, la navegación es opcional si la clave externa admite un valor NULL y, por lo tanto, la relación es opcional.

Las navegaciones de referencia de entidad de seguridad a dependiente son diferentes. En la mayoría de los casos, una entidad de seguridad puede existir siempre sin ninguna entidad dependiente. Es decir, una relación necesaria no indica que habrá siempre una entidad dependiente al menos. El modelo EF no permite de ninguna manera, ni tampoco las bases de datos relacionales, garantizar que una entidad de seguridad esté asociada a un determinado número de dependientes. Si es necesario, se debe implementar en la lógica (empresarial) de la aplicación.

Hay una excepción a esta regla: cuando los tipos de entidad de seguridad y dependiente comparten la misma tabla en una base de datos relacional o se incluyen en un documento. Esto puede ocurrir con tipos en propiedad o tipos que no son de propiedad que comparten la misma tabla. En este caso, la propiedad de navegación de entidad de seguridad a dependiente se puede marcar como necesaria, lo que indica que la dependiente debe existir.

La configuración de la propiedad de navegación como necesaria se realiza mediante el método Navigation. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Navigation(e => e.BlogHeader)
        .IsRequired();
}