Compartilhar via


Navegações de relacionamento

Os relacionamentos no EF Core são definidos por chaves estrangeiras. As navegações são sobrepostas às chaves estrangeiras para fornecer uma exibição natural e orientada a objetos para leitura e manipulação de relacionamentos. Usando navegações, os aplicativos podem trabalhar com grafos de entidades sem se preocupar com o que está acontecendo com os valores das chaves estrangeiras.

Importante

Relacionamentos múltiplos não podem compartilhar navegações. Qualquer chave estrangeira pode ser associada, no máximo, a uma navegação do principal para o dependente e, no máximo, a uma navegação do dependente para o principal.

Dica

Não é necessário tornar as navegações virtuais, a menos que estejam sendo usadas por proxies de carregamento preguiçoso ou de rastreamento de mudanças.

Navegações de referência

As navegações aparecem de duas formas: de referência e de coleção. As navegações de referência são referências simples de objetos para outra entidade. Elas representam os lados "um" dos relacionamentos de um para muitos e de um para um. Por exemplo:

public Blog TheBlog { get; set; }

As navegações de referência devem ter um setter, mas ele não precisa ser público. As navegações de referência não devem ser inicializadas automaticamente com um valor padrão não nulo, pois isso é equivalente a afirmar que uma entidade existe quando não existe.

Ao usar os tipos de referência anuláveis em C#, as navegações de referência devem ser anuláveis para relacionamentos opcionais:

public Blog? TheBlog { get; set; }

As navegações de referência para relacionamentos obrigatórios podem ser anuláveis ou não anuláveis.

Navegações de coleção

As navegações de coleção são instâncias de um tipo de coleção .NET, ou seja, qualquer tipo que esteja implementando ICollection<T>. A coleção contém instâncias do tipo de entidade relacionada, das quais pode haver qualquer número. Ela representa os lados "muitos" dos relacionamentos um para muitos e muitos para muitos. Por exemplo:

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

As navegações de coleção não precisam ter um setter. É comum inicializar a coleção no próprio código, eliminando assim a necessidade de verificar se a propriedade é null. Por exemplo:

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

Dica

Não crie acidentalmente uma propriedade de corpo de expressão, como public ICollection<Post> ThePosts => new List<Post>();. Isso criará uma nova instância de coleção vazia sempre que a propriedade for acessada e, portanto, será inútil como uma navegação.

Tipos de coleção

A instância de coleção subjacente deve implementar ICollection<T> e deve ter um método de trabalho Add. É comum usar List<T> ou HashSet<T>. List<T> é eficiente para pequenos números de entidades relacionadas e mantém uma ordenação estável. HashSet<T> tem pesquisas mais eficientes para grandes números de entidades, mas não mantém uma ordenação estável. Você também pode usar a sua própria implementação de coleção personalizada.

Importante

A coleção deve usar a igualdade de referência. Ao criar um HashSet<T> para uma navegação de coleção, sempre use ReferenceEqualityComparer.

As matrizes não podem ser usadas para navegações de coleção porque, mesmo que implementem ICollection<T>, o método Add gera uma exceção quando chamado.

Embora a instância de coleção precise ser uma ICollection<T>, a coleção não precisa ser exposta como tal. Por exemplo, é comum expor a navegação como um IEnumerable<T>, que fornece uma exibição somente leitura que não pode ser modificada aleatoriamente pelo código do aplicativo. Por exemplo:

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

Uma variação nesse padrão inclui métodos de manipulação da coleção, conforme necessário. Por exemplo:

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

O código do aplicativo ainda pode converter a coleção exposta em uma ICollection<T> e manipulá-la. Se isso for uma preocupação, a entidade poderá retornar uma cópia defensiva da coleção. Por exemplo:

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 cuidadosamente se o benefício supera a sobrecarga de criar uma cópia da coleção sempre que a navegação for acessada.

Dica

Esse padrão final funciona porque, por padrão, o EF acessa a coleção por meio de seu campo de backup. Isso significa que o próprio EF adiciona e remove entidades da coleção real, enquanto os aplicativos interagem apenas com uma cópia defensiva da coleção.

Inicialização das navegações de coleção

As navegações de coleção podem ser inicializadas pelo tipo de entidade, seja antecipadamente:

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

Ou preguiçosamente:

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

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

Se o EF precisa adicionar uma entidade a uma navegação de coleção, por exemplo, durante a execução de uma consulta, ele inicializará a coleção se, no momento, ela for null. A instância criada depende do tipo exposto da navegação.

  • Se a navegação for exposta como um HashSet<T>, uma instância de HashSet<T> usando ReferenceEqualityComparer será criada.
  • Caso contrário, se a navegação for exposta como um tipo concreto com um construtor sem parâmetros, uma instância desse tipo concreto será criada. Isso se aplica a List<T>, mas também a outros tipos de coleção, incluindo os tipos de coleção personalizados.
  • Caso contrário, se a navegação for exposta como um IEnumerable<T>, um ICollection<T> ou um ISet<T>, uma instância de HashSet<T> usando ReferenceEqualityComparer será criada.
  • Mas se a navegação for exposta como um IList<T>, uma instância de List<T> será criada.
  • Caso contrário, uma exceção será gerada.

Observação

Se as entidades de notificação, incluindo os proxies de rastreamento de mudanças, estiverem sendo usadas, ObservableCollection<T> e ObservableHashSet<T> serão usados no lugar de List<T> e HashSet<T>.

Importante

Conforme descrito na documentação do rastreamento de mudanças, o EF rastreia apenas uma única instância de qualquer entidade com um determinado valor de chave. Isso significa que as coleções usadas como navegação devem utilizar a semântica de igualdade de referência. Os tipos de entidade que não substituem a igualdade de objetos terão isso por padrão. Certifique-se de usar ReferenceEqualityComparer ao criar um HashSet<T> a ser usado como navegação para garantir que ele funcione com todos os tipos de entidade.

Configurar navegações

As navegações são incluídas no modelo como parte da configuração de um relacionamento. Ou seja, por convenção ou usando HasOne, HasMany etc. na API de criação de modelos. A maioria das configurações relacionadas às navegações é feita ao configurar o próprio relacionamento.

No entanto, existem alguns tipos de configuração que são específicos das propriedades de navegação em si, em vez de fazerem parte da configuração geral do relacionamento. Esse tipo de configuração é feito com o método Navigation. Por exemplo, para forçar o EF a acessar a navegação por meio de sua propriedade em vez de usar o campo de backup:

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

Observação

A chamada Navigation não pode ser usada para criar uma propriedade de navegação. Ela é usada apenas para configurar uma propriedade de navegação que foi previamente criada ao definir um relacionamento ou por meio de uma convenção.

Navegações obrigatórias

Uma navegação do dependente para o principal é obrigatória se o relacionamento for obrigatório, o que, por sua vez, significa que a propriedade de chave estrangeira não pode ser nula. Por outro lado, a navegação é opcional se a chave estrangeira for anulável e, portanto, o relacionamento for opcional.

As navegações de referência do principal para o dependente são diferentes. Na maioria dos casos, a entidade principal sempre pode existir sem entidades dependentes. Ou seja, um relacionamento obrigatório não indica que sempre haverá pelo menos uma entidade dependente. Não há nenhuma maneira no modelo do EF e também nenhuma maneira padrão em um banco de dados relacional de garantir que uma entidade principal esteja associada a um determinado número de dependentes. Se isso for necessário, deve ser implementado na lógica da aplicação (de negócios).

Há uma exceção a essa regra: quando o tipo principal e os dependentes estão compartilhando a mesma tabela em um banco de dados relacional ou contidos em um documento. Isso pode acontecer com tipos próprios ou não próprios compartilhando a mesma tabela. Nesse caso, a propriedade de navegação do principal para o dependente pode ser marcada como obrigatória, indicando que o dependente deve existir.

A configuração da propriedade de navegação como obrigatória é feita usando o método Navigation. Por exemplo:

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