Parte 5, Páginas do Razor com EF Core no ASP.NET Core - Modelo de Dados

Por Tom Dykstra, Jeremy Likness e Jon P. Smith

O aplicativo Web Contoso University demonstra como criar aplicativos Web das Razor Pages usando o EF Core e o Visual Studio. Para obter informações sobre a série de tutoriais, consulte o primeiro tutorial.

Se você encontrar problemas que não possa resolver, baixe o aplicativo concluído e compare esse código com o que você criou seguindo o tutorial.

Os tutoriais anteriores trabalharam com um modelo de dados básico composto por três entidades. Neste tutorial:

  • Mais entidades e relações são adicionadas.
  • O modelo de dados é personalizado com a especificação das regras de formatação, validação e mapeamento de banco de dados.

O modelo de dados concluído é mostrado na seguinte ilustração:

Entity diagram

O diagrama de banco de dados a seguir foi feito com Dataedo:

Dataedo diagram

Para criar um diagrama de banco de dados com o Dataedo:

No diagrama do Dataedo anterior, o CourseInstructor é uma tabela de junção criada pelo Entity Framework. Para obter mais informações, consulte Muitos para muitos

A entidade Student

Substitua o código em Models/Student.cs pelo seguinte código:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

O código anterior adiciona uma propriedade FullName e adiciona os seguintes atributos às propriedades existentes:

A propriedade calculada FullName

FullName é uma propriedade calculada que retorna um valor criado pela concatenação de duas outras propriedades. FullName não pode ser definido, assim, ele apenas tem um acessador get. Nenhuma coluna FullName é criada no banco de dados.

O atributo DataType

[DataType(DataType.Date)]

Para as datas de registro do aluno, todas as páginas atualmente exibem a hora do dia junto com a data, embora apenas a data seja relevante. Usando atributos de anotação de dados, você pode fazer uma alteração de código que corrigirá o formato de exibição em cada página que mostra os dados.

O atributo DataType especifica um tipo de dados mais específico do que o tipo intrínseco de banco de dados. Neste caso, apenas a data deve ser exibida, não a data e a hora. A Enumeração do Tipo de Dados fornece muitos tipos de dados, como Data, Hora, Número de Telefone, Moeda, Endereço de E-mail, etc. O atributo DataType também pode permitir que o aplicativo forneça automaticamente recursos específicos do tipo. Por exemplo:

  • O link mailto: é criado automaticamente para DataType.EmailAddress.
  • O seletor de data é fornecido para DataType.Date na maioria dos navegadores.

O atributo DataType emite atributos HTML 5 data- (pronunciados como data dash). Os atributos DataType não fornecem validação.

O atributo DisplayFormat

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

DataType.Date não especifica o formato da data exibida. Por padrão, o campo de dados é exibido de acordo com os formatos padrão com base nas CultureInfo do servidor.

O atributo DisplayFormat é usado para especificar explicitamente o formato de data. A configuração ApplyFormatInEditMode especifica que a formatação também deve ser aplicada à interface do usuário de edição. Alguns campos não devem usar ApplyFormatInEditMode. Por exemplo, o símbolo de moeda geralmente não deve ser exibido em uma caixa de texto de edição.

O atributo DisplayFormat pode ser usado por si só. Geralmente, é uma boa ideia usar o atributo DataType com o atributo DisplayFormat. O atributo DataType transmite a semântica dos dados em vez de como renderizá-los em uma tela. O atributo DataType oferece os seguintes benefícios que não estão disponíveis em DisplayFormat:

  • O navegador pode habilitar recursos do HTML5. Por exemplo, mostra um controle de calendário, o símbolo de moeda apropriado à localidade, links de email e validação de entrada do lado do cliente.
  • Por padrão, o navegador renderiza os dados usando o formato correto de acordo com a localidade.

Para obter mais informações, consulte a documentação do Auxiliar de Marcação de <input>.

O atributo StringLength

[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]

Regras de validação de dados e mensagens de erro de validação podem ser especificadas com atributos. O atributo StringLength especifica o tamanho mínimo e máximo de caracteres permitidos em um campo de dados. O código mostrado limita os nomes a, no máximo, 50 caracteres. Um exemplo que define o comprimento mínimo da cadeia de caracteres é mostrado posteriormente.

O atributo StringLength também fornece a validação do lado do cliente e do servidor. O valor mínimo não tem impacto sobre o esquema de banco de dados.

O atributo StringLength não impede que um usuário insira um espaço em branco em um nome. O atributo RegularExpression pode ser usado para aplicar restrições à entrada. Por exemplo, o seguinte código exige que o primeiro caractere esteja em maiúscula e os caracteres restantes estejam em ordem alfabética:

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

No SSOX (Pesquisador de Objetos do SQL Server), abra o designer de tabela Aluno clicando duas vezes na tabela Aluno.

Students table in SSOX before migrations

A imagem anterior mostra o esquema para a tabela Student. Os campos de nome têm o tipo nvarchar(MAX). Quando uma migração é criada e aplicada posteriormente neste tutorial, os campos de nome se tornam nvarchar(50) como resultado dos atributos de comprimento da cadeia de caracteres.

O atributo Column

[Column("FirstName")]
public string FirstMidName { get; set; }

Os atributos podem controlar como as classes e propriedades são mapeadas para o banco de dados. No modelo Student, o atributo Column é usado para mapear o nome da propriedade FirstMidName para "FirstName" no banco de dados.

Quando o banco de dados é criado, os nomes de propriedade no modelo são usados para nomes de coluna (exceto quando o atributo Column é usado). O modelo Student usa FirstMidName para o campo de nome porque o campo também pode conter um sobrenome.

Com o atributo [Column], Student.FirstMidName no modelo de dados é mapeado para a coluna FirstName da tabela Student. A adição do atributo Column altera o modelo que dá suporte ao SchoolContext. O modelo que dá suporte ao SchoolContext não corresponde mais ao banco de dados. Essa discrepância será resolvida adicionando uma migração posteriormente neste tutorial.

O atributo Required

[Required]

O atributo Required torna as propriedades de nome campos obrigatórios. O atributo Required não é necessário para tipos que não permitem valor nulo, como tipos de valor (por exemplo, DateTime, int e double). Tipos que não podem ser nulos são tratados automaticamente como campos obrigatórios.

O atributo Required precisa ser usado com MinimumLength para que MinimumLength seja imposto.

[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }

MinimumLength e Required permitem que o espaço em branco atenda à validação. Use o atributo RegularExpression para obter controle total sobre a cadeia de caracteres.

O atributo Display

[Display(Name = "Last Name")]

O atributo Display especifica que a legenda para as caixas de texto deve ser "Nome", "Sobrenome", "Nome Completo" e "Data de Inscrição". As legendas padrão não tinham espaço ao dividir as palavras, por exemplo, "Nomecompleto".

Criar uma migração

Execute o aplicativo e acesse a página Alunos. Uma exceção é gerada. O atributo [Column] faz com que o EF Espere encontrar uma coluna chamada FirstName, mas o nome da coluna no banco de dados ainda é FirstMidName.

A mensagem de erro é semelhante ao exemplo a seguir:

SqlException: Invalid column name 'FirstName'.
There are pending model changes
Pending model changes are detected in the following:

SchoolContext
  • No PMC, insira os seguintes comandos para criar uma nova migração e atualizar o banco de dados:

    Add-Migration ColumnFirstName
    Update-Database
    
    

    O primeiro desses comandos gera a seguinte mensagem de aviso:

    An operation was scaffolded that may result in the loss of data.
    Please review the migration for accuracy.
    

    O aviso é gerado porque os campos de nome agora estão limitados a 50 caracteres. Se um nome no banco de dados tiver mais de 50 caracteres, o 51º caractere até o último caractere serão perdidos.

  • Abra a tabela Alunos no SSOX:

    Students table in SSOX after migrations

    Antes de a migração ser aplicada, as colunas de nome eram do tipo nvarchar(MAX). As colunas de nome agora são nvarchar(50). O nome da coluna foi alterado de FirstMidName para FirstName.

  • Execute o aplicativo e acesse a página Alunos.
  • Observe que os horários não são inseridos nem exibidos juntamente com datas.
  • Selecione Criar Novo e tente inserir um nome com mais de 50 caracteres.

Observação

Nas seções a seguir, a criação do aplicativo em alguns estágios gera erros do compilador. As instruções especificam quando compilar o aplicativo.

A entidade Instructor

Crie Models/Instructor.cs com o seguinte código:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<Course> Courses { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

Vários atributos podem estar em uma linha. Os atributos HireDate podem ser escritos da seguinte maneira:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

As propriedades Courses e OfficeAssignment são propriedades de navegação.

Um instrutor pode ministrar qualquer quantidade de cursos e, portanto, Courses é definido como uma coleção.

public ICollection<Course> Courses { get; set; }

Um instrutor pode ter no máximo um escritório, portanto, a propriedade OfficeAssignment mantém uma única entidade OfficeAssignment. OfficeAssignment será nulo se nenhum escritório for atribuído.

public OfficeAssignment OfficeAssignment { get; set; }

A entidade OfficeAssignment

OfficeAssignment entity

Crie Models/OfficeAssignment.cs com o seguinte código:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}

O atributo Key

O atributo [Key] é usado para identificar uma propriedade como a chave primária (PK) quando o nome da propriedade é algo diferente declassnameID ou ID.

Há uma relação um para zero ou um entre as entidades Instructor e OfficeAssignment. Uma atribuição de escritório existe apenas em relação ao instrutor ao qual ela é atribuída. A PK OfficeAssignment também é a FK (chave estrangeira) da entidade Instructor. Uma relação de um para zero ou um ocorre quando uma PK em uma tabela é uma PK e uma FK em outra tabela.

EF Core não pode reconhecer automaticamente InstructorID como a PK de OfficeAssignment porque InstructorID não segue a convenção de nomenclatura de ID ou classnameID. Portanto, o atributo Key é usado para identificar InstructorID como a PK:

[Key]
public int InstructorID { get; set; }

Por padrão, o EF Core trata a chave como não gerada pelo banco de dados porque a coluna destina-se a uma relação de identificação. Para obter mais informações, consulte Chaves do EF.

A propriedade de navegação Instructor

A propriedade de navegação Instructor.OfficeAssignment pode ser nula porque pode não haver uma linha OfficeAssignment para um determinado instrutor. Um instrutor pode não ter uma atribuição de escritório.

A propriedade de navegação OfficeAssignment.Instructor sempre terá uma entidade de instrutor porque o tipo InstructorID de chave estrangeira é int, um tipo de valor não anulável. Uma atribuição de escritório não pode existir sem um instrutor.

Quando uma entidade Instructor tem uma entidade OfficeAssignment relacionada, cada entidade tem uma referência à outra em sua propriedade de navegação.

A entidade Course

Atualize Models/Course.cs com o seguinte código:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<Instructor> Instructors { get; set; }
    }
}

A entidade Course tem uma propriedade de FK (chave estrangeira) DepartmentID. DepartmentID aponta para a entidade Department relacionada. A entidade Course tem uma propriedade de navegação Department.

O EF Core não exige uma propriedade de chave estrangeira para um modelo de dados quando o modelo tem uma propriedade de navegação para uma entidade relacionada. O EF Core cria automaticamente FKs no banco de dados sempre que forem necessárias. O EF Core cria propriedades de sombra para FKs criadas automaticamente. Porém, incluir explicitamente a FK no modelo de dados pode tornar as atualizações mais simples e mais eficientes. Por exemplo, considere um modelo em que a propriedade de FK DepartmentIDnão é incluída. Quando uma entidade de curso é buscada para editar:

  • A propriedade Department será null se não for carregada de forma explícita.
  • Para atualizar a entidade de curso, a entidade Department primeiro deve ser buscada.

Quando a propriedade de FK DepartmentID está incluída no modelo de dados, não é necessário buscar a entidade Department antes de uma atualização.

O atributo DatabaseGenerated

O atributo [DatabaseGenerated(DatabaseGeneratedOption.None)] especifica que a PK é fornecida pelo aplicativo em vez de ser gerada pelo banco de dados.

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

Por padrão, o EF Core supõe que os valores de PK sejam gerados pelo banco de dados. O banco de dados gerado costuma ser a melhor abordagem. Para entidades Course, o usuário especifica o PK. Por exemplo, um número de curso, como uma série 1000 para o departamento de matemática e uma série 2000 para o departamento em inglês.

O atributo DatabaseGenerated também pode ser usado para gerar valores padrão. Por exemplo, o banco de dados pode gerar automaticamente um campo de data para registrar a data em que uma linha foi criada ou atualizada. Para obter mais informações, consulte Propriedades geradas.

Propriedades de navegação e de chave estrangeira

As propriedades de navegação e de FK (chave estrangeira) na entidade Course refletem as seguintes relações:

Um curso é atribuído a um departamento; portanto, há uma FK DepartmentID e uma propriedade de navegação Department.

public int DepartmentID { get; set; }
public Department Department { get; set; }

Um curso pode ter qualquer quantidade de estudantes inscritos; portanto, a propriedade de navegação Enrollments é uma coleção:

public ICollection<Enrollment> Enrollments { get; set; }

Um curso pode ser ministrado por vários instrutores; portanto, a propriedade de navegação Instructors é uma coleção:

public ICollection<Instructor> Instructors { get; set; }

A entidade Department

Crie Models/Department.cs com o seguinte código:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
                       ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

O atributo Column

Anteriormente, o atributo Column foi usado para alterar o mapeamento de nome de coluna. No código da entidade Department, o atributo Column é usado para alterar o mapeamento de tipo de dados SQL. A coluna Budget é definida usando o tipo de dinheiro do SQL Server no banco de dados:

[Column(TypeName="money")]
public decimal Budget { get; set; }

Em geral, o mapeamento de coluna não é necessário. O EF Core escolhe o tipo de dados do SQL Server apropriado com base no tipo de CLR da propriedade. O tipo decimal CLR é mapeado para um tipo decimal SQL Server. Budget refere-se à moeda e o tipo de dados de dinheiro é mais apropriado para moeda.

Propriedades de navegação e de chave estrangeira

As propriedades de navegação e de FK refletem as seguintes relações:

  • Um departamento pode ou não ter um administrador.
  • Um administrador é sempre um instrutor. Portanto, a propriedade InstructorID está incluída como a FK da entidade Instructor.

A propriedade de navegação é chamada Administrator, mas contém uma entidade Instructor:

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

O ? no código anterior especifica que a propriedade permite valor nulo.

Um departamento pode ter vários cursos e, portanto, há uma propriedade de navegação Courses:

public ICollection<Course> Courses { get; set; }

Por convenção, o EF Core habilita a exclusão em cascata em FKs que não permitem valor nulo e em relações de muitos para muitos. Esse comportamento padrão pode resultar em regras circulares de exclusão em cascata. As regras de exclusão em cascata circular causam uma exceção quando uma migração é adicionada.

Por exemplo, se a propriedade Department.InstructorID tiver sido definida como não anulável, o EF Core configurará uma regra de exclusão em cascata. Nesse caso, o departamento seria excluído quando o instrutor atribuído como seu administrador fosse excluído. Nesse cenário, uma regra restrita fará mais sentido. A API fluente a seguir definiria uma regra restrita e desabilitaria a exclusão em cascata.

modelBuilder.Entity<Department>()
   .HasOne(d => d.Administrator)
   .WithMany()
   .OnDelete(DeleteBehavior.Restrict)

A chave estrangeira de inscrição e as propriedades de navegação

Um registro se refere a um curso feito por um aluno.

Enrollment entity

Atualize Models/Enrollment.cs com o seguinte código:

using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}

As propriedades de navegação e de FK refletem as seguintes relações:

Um registro destina-se a um curso e, portanto, há uma propriedade de FK CourseID e uma propriedade de navegação Course:

public int CourseID { get; set; }
public Course Course { get; set; }

Um registro destina-se a um aluno e, portanto, há uma propriedade de FK StudentID e uma propriedade de navegação Student:

public int StudentID { get; set; }
public Student Student { get; set; }

Relações muitos para muitos

Há uma relação muitos para muitos entre as entidades Student e Course. A entidade Enrollment funciona como uma tabela de junção de muitos para muitos com conteúdo no banco de dados. Com conteúdo significa que a tabela Enrollment contém dados adicionais além das FKs das tabelas de junção. Na entidade Enrollment, os dados adicionais além das FKs são a PK e o Grade.

A ilustração a seguir mostra a aparência dessas relações em um diagrama de entidades. (Esse diagrama foi gerado usando o EF Power Tools para EF 6.x. A criação do diagrama não faz parte do tutorial.)

Student-Course many to many relationship

Cada linha de relação tem um 1 em uma extremidade e um asterisco (*) na outra, indicando uma relação um para muitos.

Se a tabela Enrollment não incluir informações de nota, ela apenas precisará conter as duas FKs, CourseID e StudentID. Uma tabela de junção muitos para muitos sem conteúdo é às vezes chamada de PJT (uma tabela de junção pura).

As entidades Instructor e Course têm uma relação de muitos para muitos usando uma PJT.

Atualizar o contexto de banco de dados

Atualize Data/SchoolContext.cs com o seguinte código:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable(nameof(Course))
                .HasMany(c => c.Instructors)
                .WithMany(i => i.Courses);
            modelBuilder.Entity<Student>().ToTable(nameof(Student));
            modelBuilder.Entity<Instructor>().ToTable(nameof(Instructor));
        }
    }
}

O código anterior adiciona as novas entidades e configura a relação de muitos para muitos entre as entidades Instructor e Course.

Alternativa de API fluente para atributos

O método OnModelCreating no código anterior usa a API fluente para configurar o comportamento do EF Core. A API é chamada "fluente" porque geralmente é usada pelo encadeamento de uma série de chamadas de método em uma única instrução. O seguinte código é um exemplo da API fluente:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

Neste tutorial, a API fluente é usada apenas para o mapeamento do banco de dados que não pode ser feito com atributos. No entanto, a API fluente pode especificar a maioria das regras de formatação, validação e mapeamento que pode ser feita com atributos.

Alguns atributos como MinimumLength não podem ser aplicados com a API fluente. MinimumLength não altera o esquema; apenas aplica uma regra de validação de tamanho mínimo.

Alguns desenvolvedores preferem usar a API fluente exclusivamente para que possam manter suas classes de entidade limpas. Atributos e a API fluente podem ser combinados. Há algumas configurações que apenas podem ser feitas com a API fluente, por exemplo, especificando uma PK composta. Há algumas configurações que apenas podem ser feitas com atributos (MinimumLength). A prática recomendada para uso de atributos ou da API fluente:

  • Escolha uma dessas duas abordagens.
  • Use a abordagem escolhida da forma mais consistente possível.

Alguns dos atributos usados neste tutorial são usados para:

  • Somente validação (por exemplo, MinimumLength).
  • Apenas configuração do EF Core (por exemplo, HasKey).
  • Validação e configuração do EF Core (por exemplo, [StringLength(50)]).

Para obter mais informações sobre atributos vs. API fluente, consulte Métodos de configuração.

Propagar o banco de dados

Atualize o código no Data/DbInitializer.cs:

using ContosoUniversity.Models;
using System;
using System.Collections.Generic;
using System.Linq;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var alexander = new Student
            {
                FirstMidName = "Carson",
                LastName = "Alexander",
                EnrollmentDate = DateTime.Parse("2016-09-01")
            };

            var alonso = new Student
            {
                FirstMidName = "Meredith",
                LastName = "Alonso",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var anand = new Student
            {
                FirstMidName = "Arturo",
                LastName = "Anand",
                EnrollmentDate = DateTime.Parse("2019-09-01")
            };

            var barzdukas = new Student
            {
                FirstMidName = "Gytis",
                LastName = "Barzdukas",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var li = new Student
            {
                FirstMidName = "Yan",
                LastName = "Li",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var justice = new Student
            {
                FirstMidName = "Peggy",
                LastName = "Justice",
                EnrollmentDate = DateTime.Parse("2017-09-01")
            };

            var norman = new Student
            {
                FirstMidName = "Laura",
                LastName = "Norman",
                EnrollmentDate = DateTime.Parse("2019-09-01")
            };

            var olivetto = new Student
            {
                FirstMidName = "Nino",
                LastName = "Olivetto",
                EnrollmentDate = DateTime.Parse("2011-09-01")
            };

            var students = new Student[]
            {
                alexander,
                alonso,
                anand,
                barzdukas,
                li,
                justice,
                norman,
                olivetto
            };

            context.AddRange(students);

            var abercrombie = new Instructor
            {
                FirstMidName = "Kim",
                LastName = "Abercrombie",
                HireDate = DateTime.Parse("1995-03-11")
            };

            var fakhouri = new Instructor
            {
                FirstMidName = "Fadi",
                LastName = "Fakhouri",
                HireDate = DateTime.Parse("2002-07-06")
            };

            var harui = new Instructor
            {
                FirstMidName = "Roger",
                LastName = "Harui",
                HireDate = DateTime.Parse("1998-07-01")
            };

            var kapoor = new Instructor
            {
                FirstMidName = "Candace",
                LastName = "Kapoor",
                HireDate = DateTime.Parse("2001-01-15")
            };

            var zheng = new Instructor
            {
                FirstMidName = "Roger",
                LastName = "Zheng",
                HireDate = DateTime.Parse("2004-02-12")
            };

            var instructors = new Instructor[]
            {
                abercrombie,
                fakhouri,
                harui,
                kapoor,
                zheng
            };

            context.AddRange(instructors);

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    Instructor = fakhouri,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    Instructor = harui,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    Instructor = kapoor,
                    Location = "Thompson 304" }
            };

            context.AddRange(officeAssignments);

            var english = new Department
            {
                Name = "English",
                Budget = 350000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = abercrombie
            };

            var mathematics = new Department
            {
                Name = "Mathematics",
                Budget = 100000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = fakhouri
            };

            var engineering = new Department
            {
                Name = "Engineering",
                Budget = 350000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = harui
            };

            var economics = new Department
            {
                Name = "Economics",
                Budget = 100000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = kapoor
            };

            var departments = new Department[]
            {
                english,
                mathematics,
                engineering,
                economics
            };

            context.AddRange(departments);

            var chemistry = new Course
            {
                CourseID = 1050,
                Title = "Chemistry",
                Credits = 3,
                Department = engineering,
                Instructors = new List<Instructor> { kapoor, harui }
            };

            var microeconomics = new Course
            {
                CourseID = 4022,
                Title = "Microeconomics",
                Credits = 3,
                Department = economics,
                Instructors = new List<Instructor> { zheng }
            };

            var macroeconmics = new Course
            {
                CourseID = 4041,
                Title = "Macroeconomics",
                Credits = 3,
                Department = economics,
                Instructors = new List<Instructor> { zheng }
            };

            var calculus = new Course
            {
                CourseID = 1045,
                Title = "Calculus",
                Credits = 4,
                Department = mathematics,
                Instructors = new List<Instructor> { fakhouri }
            };

            var trigonometry = new Course
            {
                CourseID = 3141,
                Title = "Trigonometry",
                Credits = 4,
                Department = mathematics,
                Instructors = new List<Instructor> { harui }
            };

            var composition = new Course
            {
                CourseID = 2021,
                Title = "Composition",
                Credits = 3,
                Department = english,
                Instructors = new List<Instructor> { abercrombie }
            };

            var literature = new Course
            {
                CourseID = 2042,
                Title = "Literature",
                Credits = 4,
                Department = english,
                Instructors = new List<Instructor> { abercrombie }
            };

            var courses = new Course[]
            {
                chemistry,
                microeconomics,
                macroeconmics,
                calculus,
                trigonometry,
                composition,
                literature
            };

            context.AddRange(courses);

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    Student = alexander,
                    Course = chemistry,
                    Grade = Grade.A
                },
                new Enrollment {
                    Student = alexander,
                    Course = microeconomics,
                    Grade = Grade.C
                },
                new Enrollment {
                    Student = alexander,
                    Course = macroeconmics,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = calculus,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = trigonometry,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = composition,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = anand,
                    Course = chemistry
                },
                new Enrollment {
                    Student = anand,
                    Course = microeconomics,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = barzdukas,
                    Course = chemistry,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = li,
                    Course = composition,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = justice,
                    Course = literature,
                    Grade = Grade.B
                }
            };

            context.AddRange(enrollments);
            context.SaveChanges();
        }
    }
}

O código anterior fornece dados de semente para as novas entidades. A maioria desse código cria novos objetos de entidade e carrega dados de exemplo. Os dados de exemplo são usados para teste.

Aplicar a migração ou remover e recriar

Com o banco de dados existente, há duas abordagens para alterar o banco de dados:

Qualquer opção funciona para o SQL Server. Embora o método apply-migration seja mais complexo e demorado, é a abordagem preferencial para ambientes de produção do mundo real.

Remover e recriar o banco de dados

Para forçar o EF Core a criar um novo banco de dados, remova e atualize o banco de dados:

  • Exclua a pasta Migração.
  • No Console do Gerenciador de Pacotes (PMC, na sigla em inglês), execute os seguintes comandos:
Drop-Database
Add-Migration InitialCreate
Update-Database

Execute o aplicativo. A execução do aplicativo executa o método DbInitializer.Initialize. O DbInitializer.Initialize preenche o novo banco de dados.

Abra o banco de dados no SSOX:

  • Se o SSOX for aberto anteriormente, clique no botão Atualizar.
  • Expanda o nó Tabelas. As tabelas criadas são exibidas.

Próximas etapas

Os próximos dois tutoriais mostram como ler e atualizar dados relacionados.

Os tutoriais anteriores trabalharam com um modelo de dados básico composto por três entidades. Neste tutorial:

  • Mais entidades e relações são adicionadas.
  • O modelo de dados é personalizado com a especificação das regras de formatação, validação e mapeamento de banco de dados.

O modelo de dados concluído é mostrado na seguinte ilustração:

Entity diagram

A entidade Student

Student entity

Substitua o código em Models/Student.cs pelo seguinte código:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

O código anterior adiciona uma propriedade FullName e adiciona os seguintes atributos às propriedades existentes:

  • [DataType]
  • [DisplayFormat]
  • [StringLength]
  • [Column]
  • [Required]
  • [Display]

A propriedade calculada FullName

FullName é uma propriedade calculada que retorna um valor criado pela concatenação de duas outras propriedades. FullName não pode ser definido, assim, ele apenas tem um acessador get. Nenhuma coluna FullName é criada no banco de dados.

O atributo DataType

[DataType(DataType.Date)]

Para as datas de registro do aluno, todas as páginas atualmente exibem a hora do dia junto com a data, embora apenas a data seja relevante. Usando atributos de anotação de dados, você pode fazer uma alteração de código que corrigirá o formato de exibição em cada página que mostra os dados.

O atributo DataType especifica um tipo de dados mais específico do que o tipo intrínseco de banco de dados. Neste caso, apenas a data deve ser exibida, não a data e a hora. A Enumeração do Tipo de Dados fornece muitos tipos de dados, como Data, Hora, Número de Telefone, Moeda, Endereço de E-mail, etc. O atributo DataType também pode permitir que o aplicativo forneça automaticamente recursos específicos do tipo. Por exemplo:

  • O link mailto: é criado automaticamente para DataType.EmailAddress.
  • O seletor de data é fornecido para DataType.Date na maioria dos navegadores.

O atributo DataType emite atributos HTML 5 data- (pronunciados como data dash). Os atributos DataType não fornecem validação.

O atributo DisplayFormat

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

DataType.Date não especifica o formato da data exibida. Por padrão, o campo de dados é exibido de acordo com os formatos padrão com base nas CultureInfo do servidor.

O atributo DisplayFormat é usado para especificar explicitamente o formato de data. A configuração ApplyFormatInEditMode especifica que a formatação também deve ser aplicada à interface do usuário de edição. Alguns campos não devem usar ApplyFormatInEditMode. Por exemplo, o símbolo de moeda geralmente não deve ser exibido em uma caixa de texto de edição.

O atributo DisplayFormat pode ser usado por si só. Geralmente, é uma boa ideia usar o atributo DataType com o atributo DisplayFormat. O atributo DataType transmite a semântica dos dados em vez de como renderizá-los em uma tela. O atributo DataType oferece os seguintes benefícios que não estão disponíveis em DisplayFormat:

  • O navegador pode habilitar recursos do HTML5. Por exemplo, mostra um controle de calendário, o símbolo de moeda apropriado à localidade, links de email e validação de entrada do lado do cliente.
  • Por padrão, o navegador renderiza os dados usando o formato correto de acordo com a localidade.

Para obter mais informações, consulte a documentação do Auxiliar de Marcação de <input>.

O atributo StringLength

[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]

Regras de validação de dados e mensagens de erro de validação podem ser especificadas com atributos. O atributo StringLength especifica o tamanho mínimo e máximo de caracteres permitidos em um campo de dados. O código mostrado limita os nomes a, no máximo, 50 caracteres. Um exemplo que define o comprimento mínimo da cadeia de caracteres é mostrado posteriormente.

O atributo StringLength também fornece a validação do lado do cliente e do servidor. O valor mínimo não tem impacto sobre o esquema de banco de dados.

O atributo StringLength não impede que um usuário insira um espaço em branco em um nome. O atributo RegularExpression pode ser usado para aplicar restrições à entrada. Por exemplo, o seguinte código exige que o primeiro caractere esteja em maiúscula e os caracteres restantes estejam em ordem alfabética:

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

No SSOX (Pesquisador de Objetos do SQL Server), abra o designer de tabela Aluno clicando duas vezes na tabela Aluno.

Students table in SSOX before migrations

A imagem anterior mostra o esquema para a tabela Student. Os campos de nome têm o tipo nvarchar(MAX). Quando uma migração é criada e aplicada posteriormente neste tutorial, os campos de nome se tornam nvarchar(50) como resultado dos atributos de comprimento da cadeia de caracteres.

O atributo Column

[Column("FirstName")]
public string FirstMidName { get; set; }

Os atributos podem controlar como as classes e propriedades são mapeadas para o banco de dados. No modelo Student, o atributo Column é usado para mapear o nome da propriedade FirstMidName para "FirstName" no banco de dados.

Quando o banco de dados é criado, os nomes de propriedade no modelo são usados para nomes de coluna (exceto quando o atributo Column é usado). O modelo Student usa FirstMidName para o campo de nome porque o campo também pode conter um sobrenome.

Com o atributo [Column], Student.FirstMidName no modelo de dados é mapeado para a coluna FirstName da tabela Student. A adição do atributo Column altera o modelo que dá suporte ao SchoolContext. O modelo que dá suporte ao SchoolContext não corresponde mais ao banco de dados. Essa discrepância será resolvida adicionando uma migração posteriormente neste tutorial.

O atributo Required

[Required]

O atributo Required torna as propriedades de nome campos obrigatórios. O atributo Required não é necessário para tipos que não permitem valor nulo, como tipos de valor (por exemplo, DateTime, int e double). Tipos que não podem ser nulos são tratados automaticamente como campos obrigatórios.

O atributo Required precisa ser usado com MinimumLength para que MinimumLength seja imposto.

[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }

MinimumLength e Required permitem que o espaço em branco atenda à validação. Use o atributo RegularExpression para obter controle total sobre a cadeia de caracteres.

O atributo Display

[Display(Name = "Last Name")]

O atributo Display especifica que a legenda para as caixas de texto deve ser "Nome", "Sobrenome", "Nome Completo" e "Data de Inscrição". As legendas padrão não tinham espaço ao dividir as palavras, por exemplo, "Nomecompleto".

Criar uma migração

Execute o aplicativo e acesse a página Alunos. Uma exceção é gerada. O atributo [Column] faz com que o EF Espere encontrar uma coluna chamada FirstName, mas o nome da coluna no banco de dados ainda é FirstMidName.

A mensagem de erro é semelhante ao exemplo a seguir:

SqlException: Invalid column name 'FirstName'.
  • No PMC, insira os seguintes comandos para criar uma nova migração e atualizar o banco de dados:

    Add-Migration ColumnFirstName
    Update-Database
    

    O primeiro desses comandos gera a seguinte mensagem de aviso:

    An operation was scaffolded that may result in the loss of data.
    Please review the migration for accuracy.
    

    O aviso é gerado porque os campos de nome agora estão limitados a 50 caracteres. Se um nome no banco de dados tiver mais de 50 caracteres, o 51º caractere até o último caractere serão perdidos.

  • Abra a tabela Alunos no SSOX:

    Students table in SSOX after migrations

    Antes de a migração ser aplicada, as colunas de nome eram do tipo nvarchar(MAX). As colunas de nome agora são nvarchar(50). O nome da coluna foi alterado de FirstMidName para FirstName.

  • Execute o aplicativo e acesse a página Alunos.
  • Observe que os horários não são inseridos nem exibidos juntamente com datas.
  • Selecione Criar Novo e tente inserir um nome com mais de 50 caracteres.

Observação

Nas seções a seguir, a criação do aplicativo em alguns estágios gera erros do compilador. As instruções especificam quando compilar o aplicativo.

A entidade Instructor

Instructor entity

Crie Models/Instructor.cs com o seguinte código:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

Vários atributos podem estar em uma linha. Os atributos HireDate podem ser escritos da seguinte maneira:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

As propriedades CourseAssignments e OfficeAssignment são propriedades de navegação.

Um instrutor pode ministrar qualquer quantidade de cursos e, portanto, CourseAssignments é definido como uma coleção.

public ICollection<CourseAssignment> CourseAssignments { get; set; }

Um instrutor pode ter no máximo um escritório, portanto, a propriedade OfficeAssignment mantém uma única entidade OfficeAssignment. OfficeAssignment será nulo se nenhum escritório for atribuído.

public OfficeAssignment OfficeAssignment { get; set; }

A entidade OfficeAssignment

OfficeAssignment entity

Crie Models/OfficeAssignment.cs com o seguinte código:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}

O atributo Key

O atributo [Key] é usado para identificar uma propriedade como a PK (chave primária) quando o nome da propriedade é algo diferente de classnameID ou ID.

Há uma relação um para zero ou um entre as entidades Instructor e OfficeAssignment. Uma atribuição de escritório existe apenas em relação ao instrutor ao qual ela é atribuída. A PK OfficeAssignment também é a FK (chave estrangeira) da entidade Instructor.

EF Core não pode reconhecer automaticamente InstructorID como a PK de OfficeAssignment porque InstructorID não segue a convenção de nomenclatura de ID ou classnameID. Portanto, o atributo Key é usado para identificar InstructorID como a PK:

[Key]
public int InstructorID { get; set; }

Por padrão, o EF Core trata a chave como não gerada pelo banco de dados porque a coluna destina-se a uma relação de identificação.

A propriedade de navegação Instructor

A propriedade de navegação Instructor.OfficeAssignment pode ser nula porque pode não haver uma linha OfficeAssignment para um determinado instrutor. Um instrutor pode não ter uma atribuição de escritório.

A propriedade de navegação OfficeAssignment.Instructor sempre terá uma entidade de instrutor porque o tipo InstructorID de chave estrangeira é int, um tipo de valor não anulável. Uma atribuição de escritório não pode existir sem um instrutor.

Quando uma entidade Instructor tem uma entidade OfficeAssignment relacionada, cada entidade tem uma referência à outra em sua propriedade de navegação.

A entidade Course

Course entity

Atualize Models/Course.cs com o seguinte código:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<CourseAssignment> CourseAssignments { get; set; }
    }
}

A entidade Course tem uma propriedade de FK (chave estrangeira) DepartmentID. DepartmentID aponta para a entidade Department relacionada. A entidade Course tem uma propriedade de navegação Department.

O EF Core não exige uma propriedade de chave estrangeira para um modelo de dados quando o modelo tem uma propriedade de navegação para uma entidade relacionada. O EF Core cria automaticamente FKs no banco de dados sempre que forem necessárias. O EF Core cria propriedades de sombra para FKs criadas automaticamente. Porém, incluir explicitamente a FK no modelo de dados pode tornar as atualizações mais simples e mais eficientes. Por exemplo, considere um modelo em que a propriedade de FK DepartmentIDnão é incluída. Quando uma entidade de curso é buscada para editar:

  • A propriedade Department será nula se não for carregada de forma explícita.
  • Para atualizar a entidade de curso, a entidade Department primeiro deve ser buscada.

Quando a propriedade de FK DepartmentID está incluída no modelo de dados, não é necessário buscar a entidade Department antes de uma atualização.

O atributo DatabaseGenerated

O atributo [DatabaseGenerated(DatabaseGeneratedOption.None)] especifica que a PK é fornecida pelo aplicativo em vez de ser gerada pelo banco de dados.

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

Por padrão, o EF Core supõe que os valores de PK sejam gerados pelo banco de dados. O banco de dados gerado costuma ser a melhor abordagem. Para entidades Course, o usuário especifica o PK. Por exemplo, um número de curso, como uma série 1000 para o departamento de matemática e uma série 2000 para o departamento em inglês.

O atributo DatabaseGenerated também pode ser usado para gerar valores padrão. Por exemplo, o banco de dados pode gerar automaticamente um campo de data para registrar a data em que uma linha foi criada ou atualizada. Para obter mais informações, consulte Propriedades geradas.

Propriedades de navegação e de chave estrangeira

As propriedades de navegação e de FK (chave estrangeira) na entidade Course refletem as seguintes relações:

Um curso é atribuído a um departamento; portanto, há uma FK DepartmentID e uma propriedade de navegação Department.

public int DepartmentID { get; set; }
public Department Department { get; set; }

Um curso pode ter qualquer quantidade de estudantes inscritos; portanto, a propriedade de navegação Enrollments é uma coleção:

public ICollection<Enrollment> Enrollments { get; set; }

Um curso pode ser ministrado por vários instrutores; portanto, a propriedade de navegação CourseAssignments é uma coleção:

public ICollection<CourseAssignment> CourseAssignments { get; set; }

CourseAssignment é explicado posteriormente.

A entidade Department

Department entity

Crie Models/Department.cs com o seguinte código:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

O atributo Column

Anteriormente, o atributo Column foi usado para alterar o mapeamento de nome de coluna. No código da entidade Department, o atributo Column é usado para alterar o mapeamento de tipo de dados SQL. A coluna Budget é definida usando o tipo de dinheiro do SQL Server no banco de dados:

[Column(TypeName="money")]
public decimal Budget { get; set; }

Em geral, o mapeamento de coluna não é necessário. O EF Core escolhe o tipo de dados do SQL Server apropriado com base no tipo de CLR da propriedade. O tipo decimal CLR é mapeado para um tipo decimal SQL Server. Budget refere-se à moeda e o tipo de dados de dinheiro é mais apropriado para moeda.

Propriedades de navegação e de chave estrangeira

As propriedades de navegação e de FK refletem as seguintes relações:

  • Um departamento pode ou não ter um administrador.
  • Um administrador é sempre um instrutor. Portanto, a propriedade InstructorID está incluída como a FK da entidade Instructor.

A propriedade de navegação é chamada Administrator, mas contém uma entidade Instructor:

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

O ponto de interrogação (?) no código anterior especifica que a propriedade permite valor nulo.

Um departamento pode ter vários cursos e, portanto, há uma propriedade de navegação Courses:

public ICollection<Course> Courses { get; set; }

Por convenção, o EF Core habilita a exclusão em cascata em FKs que não permitem valor nulo e em relações de muitos para muitos. Esse comportamento padrão pode resultar em regras circulares de exclusão em cascata. As regras de exclusão em cascata circular causam uma exceção quando uma migração é adicionada.

Por exemplo, se a propriedade Department.InstructorID tiver sido definida como não anulável, o EF Core configurará uma regra de exclusão em cascata. Nesse caso, o departamento seria excluído quando o instrutor atribuído como seu administrador fosse excluído. Nesse cenário, uma regra restrita fará mais sentido. A API fluente a seguir definiria uma regra restrita e desabilitaria a exclusão em cascata.

modelBuilder.Entity<Department>()
   .HasOne(d => d.Administrator)
   .WithMany()
   .OnDelete(DeleteBehavior.Restrict)

A entidade Enrollment

Um registro se refere a um curso feito por um aluno.

Enrollment entity

Atualize Models/Enrollment.cs com o seguinte código:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}

Propriedades de navegação e de chave estrangeira

As propriedades de navegação e de FK refletem as seguintes relações:

Um registro destina-se a um curso e, portanto, há uma propriedade de FK CourseID e uma propriedade de navegação Course:

public int CourseID { get; set; }
public Course Course { get; set; }

Um registro destina-se a um aluno e, portanto, há uma propriedade de FK StudentID e uma propriedade de navegação Student:

public int StudentID { get; set; }
public Student Student { get; set; }

Relações muitos para muitos

Há uma relação muitos para muitos entre as entidades Student e Course. A entidade Enrollment funciona como uma tabela de junção muitos para muitos com conteúdo no banco de dados. "Com conteúdo" significa que a tabela Enrollment contém dados adicionais além das FKs das tabelas unidas (nesse caso, a FK e Grade).

A ilustração a seguir mostra a aparência dessas relações em um diagrama de entidades. (Esse diagrama foi gerado usando o EF Power Tools para EF 6.x. A criação do diagrama não faz parte do tutorial.)

Student-Course many to many relationship

Cada linha de relação tem um 1 em uma extremidade e um asterisco (*) na outra, indicando uma relação um para muitos.

Se a tabela Enrollment não incluir informações de nota, ela apenas precisará conter as duas FKs (CourseID e StudentID). Uma tabela de junção muitos para muitos sem conteúdo é às vezes chamada de PJT (uma tabela de junção pura).

As entidades Instructor e Course têm uma relação muitos para muitos usando uma tabela de junção pura.

Observação: o EF 6.x é compatível com tabelas de junção implícita para relações de muitos para muitos, mas o EF Core não é. Para obter mais informações, consulte Relações de muitos para muitos no EF Core 2.0.

A entidade CourseAssignment

CourseAssignment entity

Crie Models/CourseAssignment.cs com o seguinte código:

namespace ContosoUniversity.Models
{
    public class CourseAssignment
    {
        public int InstructorID { get; set; }
        public int CourseID { get; set; }
        public Instructor Instructor { get; set; }
        public Course Course { get; set; }
    }
}

O relacionamento de muitos para muitos do instrutor para cursos requer uma tabela de junção e a entidade para essa tabela de junção é CourseAssignment.

Instructor-to-Courses m:M

É comum nomear uma entidade de junção EntityName1EntityName2. Por exemplo, a tabela de junção Instrutor para Cursos com esse padrão seria CourseInstructor. No entanto, recomendamos que você use um nome que descreve a relação.

Modelos de dados começam simples e aumentam. As PJTs (tabelas de junção sem payload) costumam evoluir para incluir a payload. Começando com um nome descritivo de entidade, o nome não precisa ser alterado quando a tabela de junção é alterada. O ideal é que a entidade de junção tenha seu próprio nome natural (possivelmente, uma única palavra) no domínio de negócios. Por exemplo, Manuais e Clientes podem ser vinculados com uma entidade de junção chamada Ratings. Para a relação muitos para muitos de Instrutor para Cursos, CourseAssignment é preferível a CourseInstructor.

Chave composta

As duas FKs em CourseAssignment (InstructorID e CourseID) juntas identificam exclusivamente cada linha da tabela CourseAssignment. CourseAssignment não exige um PK dedicado. As propriedades InstructorID e CourseID funcionam como uma PK composta. A única maneira de especificar PKs compostas no EF Core é com a API fluente. A próxima seção mostra como configurar a PK composta.

A chave composta garante que:

  • Várias linhas são permitidas para um curso.
  • Várias linhas são permitidas para um instrutor.
  • Não sejam permitidas várias linhas para o mesmo instrutor e curso.

A entidade de junção Enrollment define sua própria PK e, portanto, duplicatas desse tipo são possíveis. Para impedir duplicatas como essas:

  • Adicione um índice exclusivo nos campos de FK ou
  • Configure Enrollment com uma chave primária composta semelhante a CourseAssignment. Para obter mais informações, consulte Índices.

Atualizar o contexto de banco de dados

Atualize Data/SchoolContext.cs com o seguinte código:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

O código anterior adiciona novas entidades e configura a PK composta da entidade CourseAssignment.

Alternativa de API fluente para atributos

O método OnModelCreating no código anterior usa a API fluente para configurar o comportamento do EF Core. A API é chamada "fluente" porque geralmente é usada pelo encadeamento de uma série de chamadas de método em uma única instrução. O seguinte código é um exemplo da API fluente:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

Neste tutorial, a API fluente é usada apenas para o mapeamento do banco de dados que não pode ser feito com atributos. No entanto, a API fluente pode especificar a maioria das regras de formatação, validação e mapeamento que pode ser feita com atributos.

Alguns atributos como MinimumLength não podem ser aplicados com a API fluente. MinimumLength não altera o esquema; apenas aplica uma regra de validação de tamanho mínimo.

Alguns desenvolvedores preferem usar a API fluente exclusivamente para que possam manter suas classes de entidade "limpas". Atributos e a API fluente podem ser misturados. Há algumas configurações que apenas podem ser feitas com a API fluente (especificando uma PK composta). Há algumas configurações que apenas podem ser feitas com atributos (MinimumLength). A prática recomendada para uso de atributos ou da API fluente:

  • Escolha uma dessas duas abordagens.
  • Use a abordagem escolhida da forma mais consistente possível.

Alguns dos atributos usados neste tutorial são usados para:

  • Somente validação (por exemplo, MinimumLength).
  • Apenas configuração do EF Core (por exemplo, HasKey).
  • Validação e configuração do EF Core (por exemplo, [StringLength(50)]).

Para obter mais informações sobre atributos vs. API fluente, consulte Métodos de configuração.

Diagrama de entidade

A ilustração a seguir mostra o diagrama criado pelo EF Power Tools para o modelo Escola concluído.

Entity diagram

O diagrama acima mostra:

  • Várias linhas de relação um-para-muitos (1 para *).
  • A linha de relação um para zero ou um (1 para 0..1) entre as entidades Instructor e OfficeAssignment.
  • A linha de relação zero-ou-um-para-muitos (0..1 para *) entre as entidades Instructor e Department.

Propagar o banco de dados

Atualize o código no Data/DbInitializer.cs:

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            //context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander",
                    EnrollmentDate = DateTime.Parse("2016-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",
                    EnrollmentDate = DateTime.Parse("2019-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",
                    EnrollmentDate = DateTime.Parse("2017-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",
                    EnrollmentDate = DateTime.Parse("2019-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",
                    EnrollmentDate = DateTime.Parse("2011-09-01") }
            };

            context.Students.AddRange(students);
            context.SaveChanges();

            var instructors = new Instructor[]
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie",
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",
                    HireDate = DateTime.Parse("2004-02-12") }
            };

            context.Instructors.AddRange(instructors);
            context.SaveChanges();

            var departments = new Department[]
            {
                new Department { Name = "English",     Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
                new Department { Name = "Mathematics", Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
                new Department { Name = "Engineering", Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
                new Department { Name = "Economics",   Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
            };

            context.Departments.AddRange(departments);
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
            };

            context.Courses.AddRange(courses);
            context.SaveChanges();

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
                    Location = "Thompson 304" },
            };

            context.OfficeAssignments.AddRange(officeAssignments);
            context.SaveChanges();

            var courseInstructors = new CourseAssignment[]
            {
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
            };

            context.CourseAssignments.AddRange(courseInstructors);
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    Grade = Grade.A
                },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    Grade = Grade.C
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B
                    },
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Li").ID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Justice").ID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B
                    }
            };

            foreach (Enrollment e in enrollments)
            {
                var enrollmentInDataBase = context.Enrollments.Where(
                    s =>
                            s.Student.ID == e.StudentID &&
                            s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollments.Add(e);
                }
            }
            context.SaveChanges();
        }
    }
}

O código anterior fornece dados de semente para as novas entidades. A maioria desse código cria novos objetos de entidade e carrega dados de exemplo. Os dados de exemplo são usados para teste. Consulte Enrollments e CourseAssignments para obter exemplos de como tabelas de junção muitos para muitos podem ser propagadas.

Adicionar uma migração

Compile o projeto.

No PMC, execute o seguinte comando.

Add-Migration ComplexDataModel

O comando anterior exibe um aviso sobre a possível perda de dados.

An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
To undo this action, use 'ef migrations remove'

Se o comando database update é executado, o seguinte erro é produzido:

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.

Na próxima seção, você verá o que fazer sobre esse erro.

Aplicar a migração ou remover e recriar

Agora que você tem um banco de dados existente, precisa pensar sobre como aplicar as alterações a ele. Este tutorial mostra duas alternativas:

Qualquer opção funciona para o SQL Server. Embora o método apply-migration seja mais complexo e demorado, é a abordagem preferencial para ambientes de produção do mundo real.

Remover e recriar o banco de dados

Ignore esta seção se você estiver usando SQL Server e desejar fazer a abordagem de migração de aplicação na seção a seguir.

Para forçar o EF Core a criar um novo banco de dados, remova e atualize o banco de dados:

  • No PMC (Console do Gerenciador de Pacotes), execute o seguinte comando:

    Drop-Database
    
  • Exclua a pasta Migrations e execute o seguinte comando:

    Add-Migration InitialCreate
    Update-Database
    

Execute o aplicativo. A execução do aplicativo executa o método DbInitializer.Initialize. O DbInitializer.Initialize preenche o novo banco de dados.

Abra o banco de dados no SSOX:

  • Se o SSOX for aberto anteriormente, clique no botão Atualizar.

  • Expanda o nó Tabelas. As tabelas criadas são exibidas.

    Tables in SSOX

  • Examine a tabela CourseAssignment:

    • Clique com o botão direito do mouse na tabela CourseAssignment e selecione Exibir Dados.
    • Verifique se a tabela CourseAssignment contém dados.

    CourseAssignment data in SSOX

Aplicar a migração

Esta seção é opcional. Estas etapas só funcionarão para o LocalDB do SQL Server e apenas se você tiver ignorado a seção Remover e recriar o banco de dados anterior.

Quando as migrações são executadas com os dados existentes, pode haver restrições de FK que não são atendidas com os dados existentes. Com os dados de produção, é necessário executar etapas para migrar os dados existentes. Esta seção fornece um exemplo de correção de violações de restrição de FK. Não faça essas alterações de código sem um backup. Não faça essas alterações de código se você tiver concluído a seção Remover e recriar o banco de dados anterior.

O arquivo {timestamp}_ComplexDataModel.cs contém o seguinte código:

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    type: "int",
    nullable: false,
    defaultValue: 0);

O código anterior adiciona uma FK DepartmentID que não permite valor nulo à tabela Course. O banco de dados do tutorial anterior contém linhas em Course e, portanto, essa tabela não pode ser atualizada por migrações.

Para fazer a migração ComplexDataModel funcionar com os dados existentes:

  • Altere o código para dar à nova coluna (DepartmentID) um valor padrão.
  • Crie um departamento fictício chamado "Temp" para atuar como o departamento padrão.

Corrigir as restrições de chave estrangeira

Na classe de migração ComplexDataModel, atualize o método Up:

  • Abra o arquivo {timestamp}_ComplexDataModel.cs .
  • Comente a linha de código que adiciona a coluna DepartmentID à tabela Course.
migrationBuilder.AlterColumn<string>(
    name: "Title",
    table: "Course",
    maxLength: 50,
    nullable: true,
    oldClrType: typeof(string),
    oldNullable: true);
            
//migrationBuilder.AddColumn<int>(
//    name: "DepartmentID",
//    table: "Course",
//    nullable: false,
//    defaultValue: 0);

Adicione o código realçado a seguir. O novo código é inserido após o bloco .CreateTable( name: "Department":

migrationBuilder.CreateTable(
    name: "Department",
    columns: table => new
    {
        DepartmentID = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Budget = table.Column<decimal>(type: "money", nullable: false),
        InstructorID = table.Column<int>(type: "int", nullable: true),
        Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
        StartDate = table.Column<DateTime>(type: "datetime2", nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Department", x => x.DepartmentID);
        table.ForeignKey(
            name: "FK_Department_Instructor_InstructorID",
            column: x => x.InstructorID,
            principalTable: "Instructor",
            principalColumn: "ID",
            onDelete: ReferentialAction.Restrict);
    });

 migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    nullable: false,
    defaultValue: 1);

Com as alterações anteriores, as linhas Course existentes estarão relacionadas ao departamento "Temp" após a execução do método ComplexDataModel.Up.

A maneira de lidar com a situação mostrada aqui é simplificada para este tutorial. Um aplicativo de produção:

  • Inclui código ou scripts para adicionar linhas Department e linhas Course relacionadas às novas linhas Department.
  • Não usa o departamento "Temp" nem o valor padrão para Course.DepartmentID.
  • No PMC (Console do Gerenciador de Pacotes), execute o seguinte comando:

    Update-Database
    

Como o método DbInitializer.Initialize foi projetado para funcionar apenas com um banco de dados vazio, use SSOX para excluir todas as linhas nas tabelas Student e Course. (A exclusão em cascata cuidará da tabela de Registro.)

Execute o aplicativo. A execução do aplicativo executa o método DbInitializer.Initialize. O DbInitializer.Initialize preenche o novo banco de dados.

Próximas etapas

Os próximos dois tutoriais mostram como ler e atualizar dados relacionados.

Os tutoriais anteriores trabalharam com um modelo de dados básico composto por três entidades. Neste tutorial:

  • Mais entidades e relações são adicionadas.
  • O modelo de dados é personalizado com a especificação das regras de formatação, validação e mapeamento de banco de dados.

As classes de entidade para o modelo de dados concluído são mostradas na seguinte ilustração:

Entity diagram

Caso tenha problemas que não consiga resolver, baixe o aplicativo concluído.

Personalizar o modelo de dados com atributos

Nesta seção, o modelo de dados é personalizado com atributos.

O atributo DataType

As páginas de alunos atualmente exibem a hora da data de registro. Normalmente, os campos de data mostram apenas a data e não a hora.

Atualize Models/Student.cs com o seguinte código realçado:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

O atributo DataType especifica um tipo de dados mais específico do que o tipo intrínseco de banco de dados. Neste caso, apenas a data deve ser exibida, não a data e a hora. A Enumeração do Tipo de Dados fornece muitos tipos de dados, como Data, Hora, Número de Telefone, Moeda, Endereço de E-mail, etc. O atributo DataType também pode permitir que o aplicativo forneça automaticamente recursos específicos do tipo. Por exemplo:

  • O link mailto: é criado automaticamente para DataType.EmailAddress.
  • O seletor de data é fornecido para DataType.Date na maioria dos navegadores.

O atributo DataType emite atributos data- HTML 5 (pronunciados “data dash”) que são consumidos pelos navegadores HTML 5. Os atributos DataType não fornecem validação.

DataType.Date não especifica o formato da data exibida. Por padrão, o campo de dados é exibido de acordo com os formatos padrão com base nas CultureInfo do servidor.

O atributo DisplayFormat é usado para especificar explicitamente o formato de data:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

A configuração ApplyFormatInEditMode especifica que a formatação também deve ser aplicada à interface do usuário de edição. Alguns campos não devem usar ApplyFormatInEditMode. Por exemplo, o símbolo de moeda geralmente não deve ser exibido em uma caixa de texto de edição.

O atributo DisplayFormat pode ser usado por si só. Geralmente, é uma boa ideia usar o atributo DataType com o atributo DisplayFormat. O atributo DataType transmite a semântica dos dados em vez de como renderizá-los em uma tela. O atributo DataType oferece os seguintes benefícios que não estão disponíveis em DisplayFormat:

  • O navegador pode habilitar recursos do HTML5. Por exemplo, mostra um controle de calendário, o símbolo de moeda apropriado à localidade, links de email, validação de entrada do lado do cliente, etc.
  • Por padrão, o navegador renderiza os dados usando o formato correto de acordo com a localidade.

Para obter mais informações, consulte a documentação do Auxiliar de Marcação de <input>.

Execute o aplicativo. Navegue para a página Índice de Alunos. As horas não são mais exibidas. Cada exibição que usa o modelo Student exibe a data sem a hora.

Students index page showing dates without times

O atributo StringLength

Regras de validação de dados e mensagens de erro de validação podem ser especificadas com atributos. O atributo StringLength especifica o tamanho mínimo e máximo de caracteres permitidos em um campo de dados. O atributo StringLength também fornece a validação do lado do cliente e do servidor. O valor mínimo não tem impacto sobre o esquema de banco de dados.

Atualize o modelo Student com o seguinte código:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

O código anterior limita os nomes a, no máximo, 50 caracteres. O atributo StringLength não impede que um usuário insira um espaço em branco em um nome. O atributo RegularExpression é usado para aplicar restrições à entrada. Por exemplo, o seguinte código exige que o primeiro caractere esteja em maiúscula e os caracteres restantes estejam em ordem alfabética:

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

Executar o aplicativo:

  • Navegue para a página Alunos.
  • Selecione Criar Novo e insira um nome com mais de 50 caracteres.
  • Selecione Criar e a validação do lado do cliente mostrará uma mensagem de erro.

Students index page showing string length errors

No SSOX (Pesquisador de Objetos do SQL Server), abra o designer de tabela Aluno clicando duas vezes na tabela Aluno.

Students table in SSOX before migrations

A imagem anterior mostra o esquema para a tabela Student. Os campos de nome têm o tipo nvarchar(MAX) porque as migrações não foram executadas no BD. Quando as migrações forem executadas mais adiante neste tutorial, os campos de nome se tornarão nvarchar(50).

O atributo Column

Os atributos podem controlar como as classes e propriedades são mapeadas para o banco de dados. Nesta seção, o atributo Column é usado para mapear o nome da propriedade FirstMidName como "FirstName" no BD.

Quando o BD é criado, os nomes de propriedade no modelo são usados para nomes de coluna (exceto quando o atributo Column é usado).

O modelo Student usa FirstMidName para o campo de nome porque o campo também pode conter um sobrenome.

Atualize o arquivo Student.cs com o seguinte código realçado:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Com a alteração anterior, Student.FirstMidName no aplicativo é mapeado para a coluna FirstName da tabela Student.

A adição do atributo Column altera o modelo que dá suporte ao SchoolContext. O modelo que dá suporte ao SchoolContext não corresponde mais ao banco de dados. Se o aplicativo for executado antes da aplicação das migrações, a seguinte exceção será gerada:

SqlException: Invalid column name 'FirstName'.

Para atualizar o BD:

  • Compile o projeto.
  • Abra uma janela Comando na pasta do projeto. Insira os seguintes comandos para criar uma nova migração e atualizar o BD:
Add-Migration ColumnFirstName
Update-Database

O comando migrations add ColumnFirstName gera a seguinte mensagem de aviso:

An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.

O aviso é gerado porque os campos de nome agora estão limitados a 50 caracteres. Se um nome no BD tiver mais de 50 caracteres, o 51º caractere até o último caractere serão perdidos.

  • Testar o aplicativo.

Abra a tabela Alunos no SSOX:

Students table in SSOX after migrations

Antes de a migração ser aplicada, as colunas de nome eram do tipo nvarchar(MAX). As colunas de nome agora são nvarchar(50). O nome da coluna foi alterado de FirstMidName para FirstName.

Observação

Na seção a seguir, a criação do aplicativo em alguns estágios gera erros do compilador. As instruções especificam quando compilar o aplicativo.

Atualização da entidade Student

Student entity

Atualize Models/Student.cs com o seguinte código:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

O atributo Required

O atributo Required torna as propriedades de nome campos obrigatórios. O atributo Required não é necessário para tipos que não permitem valor nulo, como tipos de valor (DateTime, int, double, etc.). Tipos que não podem ser nulos são tratados automaticamente como campos obrigatórios.

O atributo Required pode ser substituído por um parâmetro de tamanho mínimo no atributo StringLength:

[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }

O atributo Display

O atributo Display especifica que a legenda para as caixas de texto deve ser "Nome", "Sobrenome", "Nome Completo" e "Data de Inscrição". As legendas padrão não tinham espaço ao dividir as palavras, por exemplo, "Nomecompleto".

A propriedade calculada FullName

FullName é uma propriedade calculada que retorna um valor criado pela concatenação de duas outras propriedades. FullName não pode ser definido; ele apenas tem um acessador get. Nenhuma coluna FullName é criada no banco de dados.

Criar a entidade Instructor

Instructor entity

Crie Models/Instructor.cs com o seguinte código:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

Vários atributos podem estar em uma linha. Os atributos HireDate podem ser escritos da seguinte maneira:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

As propriedades de navegação CourseAssignments e OfficeAssignment

As propriedades CourseAssignments e OfficeAssignment são propriedades de navegação.

Um instrutor pode ministrar qualquer quantidade de cursos e, portanto, CourseAssignments é definido como uma coleção.

public ICollection<CourseAssignment> CourseAssignments { get; set; }

Se uma propriedade de navegação armazenar várias entidades:

  • Ele deve ser um tipo de lista no qual as entradas possam ser adicionadas, excluídas e atualizadas.

Os tipos de propriedade de navegação incluem:

  • ICollection<T>
  • List<T>
  • HashSet<T>

Se ICollection<T> for especificado, o EF Core criará uma coleção HashSet<T> por padrão.

A entidade CourseAssignment é explicada na seção sobre relações muitos para muitos.

Regras de negócio do Contoso University indicam que um instrutor pode ter, no máximo, um escritório. A propriedade OfficeAssignment contém uma única entidade OfficeAssignment. OfficeAssignment será nulo se nenhum escritório for atribuído.

public OfficeAssignment OfficeAssignment { get; set; }

Criar a entidade OfficeAssignment

OfficeAssignment entity

Crie Models/OfficeAssignment.cs com o seguinte código:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}

O atributo Key

O atributo [Key] é usado para identificar uma propriedade como a PK (chave primária) quando o nome da propriedade é algo diferente de classnameID ou ID.

Há uma relação um para zero ou um entre as entidades Instructor e OfficeAssignment. Uma atribuição de escritório existe apenas em relação ao instrutor ao qual ela é atribuída. A PK OfficeAssignment também é a FK (chave estrangeira) da entidade Instructor. O EF Core não pode reconhecer automaticamente o InstructorID como a PK do OfficeAssignment porque:

  • InstructorID não segue a convenção de nomenclatura de ID nem de classnameID.

Portanto, o atributo Key é usado para identificar InstructorID como a PK:

[Key]
public int InstructorID { get; set; }

Por padrão, o EF Core trata a chave como não gerada pelo banco de dados porque a coluna destina-se a uma relação de identificação.

A propriedade de navegação Instructor

A propriedade de navegação OfficeAssignment da entidade Instructor permite valor nulo porque:

  • Tipos de referência (como classes que permitem valor nulo).
  • Um instrutor pode não ter uma atribuição de escritório.

A entidade OfficeAssignment tem uma propriedade de navegação Instructor que não permite valor nulo porque:

  • InstructorID não permite valor nulo.
  • Uma atribuição de escritório não pode existir sem um instrutor.

Quando uma entidade Instructor tem uma entidade OfficeAssignment relacionada, cada entidade tem uma referência à outra em sua propriedade de navegação.

O atributo [Required] pode ser aplicado à propriedade de navegação Instructor:

[Required]
public Instructor Instructor { get; set; }

O código anterior especifica que deve haver um instrutor relacionado. O código anterior é desnecessário porque a chave estrangeira InstructorID (que também é a PK) não permite valor nulo.

Modificar a entidade Course

Course entity

Atualize Models/Course.cs com o seguinte código:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<CourseAssignment> CourseAssignments { get; set; }
    }
}

A entidade Course tem uma propriedade de FK (chave estrangeira) DepartmentID. DepartmentID aponta para a entidade Department relacionada. A entidade Course tem uma propriedade de navegação Department.

O EF Core não exige uma propriedade de FK para um modelo de dados quando o modelo tem uma propriedade de navegação para uma entidade relacionada.

O EF Core cria automaticamente FKs no banco de dados sempre que forem necessárias. O EF Core cria propriedades de sombra para FKs criadas automaticamente. Ter a FK no modelo de dados pode tornar as atualizações mais simples e mais eficientes. Por exemplo, considere um modelo em que a propriedade de FK DepartmentIDnão é incluída. Quando uma entidade de curso é buscada para editar:

  • A entidade Department será nula se não for carregada de forma explícita.
  • Para atualizar a entidade de curso, a entidade Department primeiro deve ser buscada.

Quando a propriedade de FK DepartmentID está incluída no modelo de dados, não é necessário buscar a entidade Department antes de uma atualização.

O atributo DatabaseGenerated

O atributo [DatabaseGenerated(DatabaseGeneratedOption.None)] especifica que a PK é fornecida pelo aplicativo em vez de ser gerada pelo banco de dados.

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

Por padrão, o EF Core supõe que os valores de PK sejam gerados pelo BD. Os valores de PK gerados pelo BD geralmente são a melhor abordagem. Para entidades Course, o usuário especifica o PK. Por exemplo, um número de curso, como uma série 1000 para o departamento de matemática e uma série 2000 para o departamento em inglês.

O atributo DatabaseGenerated também pode ser usado para gerar valores padrão. Por exemplo, o BD pode gerar automaticamente um campo de data para registrar a data em que uma linha foi criada ou atualizada. Para obter mais informações, consulte Propriedades geradas.

Propriedades de navegação e de chave estrangeira

As propriedades de navegação e de FK (chave estrangeira) na entidade Course refletem as seguintes relações:

Um curso é atribuído a um departamento; portanto, há uma FK DepartmentID e uma propriedade de navegação Department.

public int DepartmentID { get; set; }
public Department Department { get; set; }

Um curso pode ter qualquer quantidade de estudantes inscritos; portanto, a propriedade de navegação Enrollments é uma coleção:

public ICollection<Enrollment> Enrollments { get; set; }

Um curso pode ser ministrado por vários instrutores; portanto, a propriedade de navegação CourseAssignments é uma coleção:

public ICollection<CourseAssignment> CourseAssignments { get; set; }

CourseAssignment é explicado posteriormente.

Criar a entidade Department

Department entity

Crie Models/Department.cs com o seguinte código:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

O atributo Column

Anteriormente, o atributo Column foi usado para alterar o mapeamento de nome de coluna. No código da entidade Department, o atributo Column é usado para alterar o mapeamento de tipo de dados SQL. A coluna Budget é definida usando o tipo de dinheiro do SQL Server no BD:

[Column(TypeName="money")]
public decimal Budget { get; set; }

Em geral, o mapeamento de coluna não é necessário. Em geral, o EF Core escolhe o tipo de dados do SQL Server apropriado com base no tipo CLR da propriedade. O tipo decimal CLR é mapeado para um tipo decimal SQL Server. Budget refere-se à moeda e o tipo de dados de dinheiro é mais apropriado para moeda.

Propriedades de navegação e de chave estrangeira

As propriedades de navegação e de FK refletem as seguintes relações:

  • Um departamento pode ou não ter um administrador.
  • Um administrador é sempre um instrutor. Portanto, a propriedade InstructorID está incluída como a FK da entidade Instructor.

A propriedade de navegação é chamada Administrator, mas contém uma entidade Instructor:

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

O ponto de interrogação (?) no código anterior especifica que a propriedade permite valor nulo.

Um departamento pode ter vários cursos e, portanto, há uma propriedade de navegação Courses:

public ICollection<Course> Courses { get; set; }

Observação: por convenção, o EF Core habilita a exclusão em cascata em FKs que não permitem valor nulo e em relações de muitos para muitos. A exclusão em cascata pode resultar em regras de exclusão em cascata circular. As regras de exclusão em cascata circular causam uma exceção quando uma migração é adicionada.

Por exemplo, se a propriedade Department.InstructorID tiver sido definida como não anulável:

  • O EF Core configura uma regra de exclusão em cascata para excluir o departamento quando o instrutor é excluído.

  • Excluir o departamento quando o instrutor é excluído não é o comportamento desejado.

  • A seguinte API fluente definiria uma regra de restrição em vez de em cascata.

    modelBuilder.Entity<Department>()
        .HasOne(d => d.Administrator)
        .WithMany()
        .OnDelete(DeleteBehavior.Restrict)
    

O código anterior desabilita a exclusão em cascata na relação departamento-instrutor.

Atualizar a entidade Enrollment

Um registro se refere a um curso feito por um aluno.

Enrollment entity

Atualize Models/Enrollment.cs com o seguinte código:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}

Propriedades de navegação e de chave estrangeira

As propriedades de navegação e de FK refletem as seguintes relações:

Um registro destina-se a um curso e, portanto, há uma propriedade de FK CourseID e uma propriedade de navegação Course:

public int CourseID { get; set; }
public Course Course { get; set; }

Um registro destina-se a um aluno e, portanto, há uma propriedade de FK StudentID e uma propriedade de navegação Student:

public int StudentID { get; set; }
public Student Student { get; set; }

Relações muitos para muitos

Há uma relação muitos para muitos entre as entidades Student e Course. A entidade Enrollment funciona como uma tabela de junção muitos para muitos com conteúdo no banco de dados. "Com conteúdo" significa que a tabela Enrollment contém dados adicionais além das FKs das tabelas unidas (nesse caso, a FK e Grade).

A ilustração a seguir mostra a aparência dessas relações em um diagrama de entidades. (Esse diagrama foi gerado usando o EF Power Tools para EF 6.x. A criação do diagrama não faz parte do tutorial.)

Student-Course many to many relationship

Cada linha de relação tem um 1 em uma extremidade e um asterisco (*) na outra, indicando uma relação um para muitos.

Se a tabela Enrollment não incluir informações de nota, ela apenas precisará conter as duas FKs (CourseID e StudentID). Uma tabela de junção muitos para muitos sem conteúdo é às vezes chamada de PJT (uma tabela de junção pura).

As entidades Instructor e Course têm uma relação muitos para muitos usando uma tabela de junção pura.

Observação: o EF 6.x é compatível com tabelas de junção implícita para relações de muitos para muitos, mas o EF Core não é. Para obter mais informações, consulte Relações de muitos para muitos no EF Core 2.0.

A entidade CourseAssignment

CourseAssignment entity

Crie Models/CourseAssignment.cs com o seguinte código:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class CourseAssignment
    {
        public int InstructorID { get; set; }
        public int CourseID { get; set; }
        public Instructor Instructor { get; set; }
        public Course Course { get; set; }
    }
}

Instrutor para Cursos

Instructor-to-Courses m:M

A relação muitos para muitos de Instrutor para Cursos:

  • Exige que uma tabela de junção seja representada por um conjunto de entidades.
  • É uma tabela de junção pura (tabela sem conteúdo).

É comum nomear uma entidade de junção EntityName1EntityName2. Por exemplo, a tabela de junção Instrutor para Cursos com esse padrão é CourseInstructor. No entanto, recomendamos que você use um nome que descreve a relação.

Modelos de dados começam simples e aumentam. PJTs (junções sem conteúdo) evoluem com frequência para incluir o conteúdo. Começando com um nome descritivo de entidade, o nome não precisa ser alterado quando a tabela de junção é alterada. O ideal é que a entidade de junção tenha seu próprio nome natural (possivelmente, uma única palavra) no domínio de negócios. Por exemplo, Manuais e Clientes podem ser vinculados com uma entidade de junção chamada Ratings. Para a relação muitos para muitos de Instrutor para Cursos, CourseAssignment é preferível a CourseInstructor.

Chave composta

As FKs não permitem valor nulo. As duas FKs em CourseAssignment (InstructorID e CourseID) juntas identificam exclusivamente cada linha da tabela CourseAssignment. CourseAssignment não exige um PK dedicado. As propriedades InstructorID e CourseID funcionam como uma PK composta. A única maneira de especificar PKs compostas no EF Core é com a API fluente. A próxima seção mostra como configurar a PK composta.

A chave composta garante:

  • Várias linhas são permitidas para um curso.
  • Várias linhas são permitidas para um instrutor.
  • Não é permitido ter várias linhas para o mesmo instrutor e curso.

A entidade de junção Enrollment define sua própria PK e, portanto, duplicatas desse tipo são possíveis. Para impedir duplicatas como essas:

  • Adicione um índice exclusivo nos campos de FK ou
  • Configure Enrollment com uma chave primária composta semelhante a CourseAssignment. Para obter mais informações, consulte Índices.

Atualizar o contexto de BD

Adicione o código realçado a seguir a Data/SchoolContext.cs:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Models
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollment { get; set; }
        public DbSet<Student> Student { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

O código anterior adiciona novas entidades e configura a PK composta da entidade CourseAssignment.

Alternativa de API fluente para atributos

O método OnModelCreating no código anterior usa a API fluente para configurar o comportamento do EF Core. A API é chamada "fluente" porque geralmente é usada pelo encadeamento de uma série de chamadas de método em uma única instrução. O seguinte código é um exemplo da API fluente:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

Neste tutorial, a API fluente é usada apenas para o mapeamento do BD que não pode ser feito com atributos. No entanto, a API fluente pode especificar a maioria das regras de formatação, validação e mapeamento que pode ser feita com atributos.

Alguns atributos como MinimumLength não podem ser aplicados com a API fluente. MinimumLength não altera o esquema; apenas aplica uma regra de validação de tamanho mínimo.

Alguns desenvolvedores preferem usar a API fluente exclusivamente para que possam manter suas classes de entidade "limpas". Atributos e a API fluente podem ser misturados. Há algumas configurações que apenas podem ser feitas com a API fluente (especificando uma PK composta). Há algumas configurações que apenas podem ser feitas com atributos (MinimumLength). A prática recomendada para uso de atributos ou da API fluente:

  • Escolha uma dessas duas abordagens.
  • Use a abordagem escolhida da forma mais consistente possível.

Alguns dos atributos usados neste tutorial são usados para:

  • Somente validação (por exemplo, MinimumLength).
  • Apenas configuração do EF Core (por exemplo, HasKey).
  • Validação e configuração do EF Core (por exemplo, [StringLength(50)]).

Para obter mais informações sobre atributos vs. API fluente, consulte Métodos de configuração.

Diagrama de entidade mostrando relações

A ilustração a seguir mostra o diagrama criado pelo EF Power Tools para o modelo Escola concluído.

Entity diagram

O diagrama acima mostra:

  • Várias linhas de relação um-para-muitos (1 para *).
  • A linha de relação um para zero ou um (1 para 0..1) entre as entidades Instructor e OfficeAssignment.
  • A linha de relação zero-ou-um-para-muitos (0..1 para *) entre as entidades Instructor e Department.

Propagar o BD com os dados de teste

Atualize o código no Data/DbInitializer.cs:

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            //context.Database.EnsureCreated();

            // Look for any students.
            if (context.Student.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander",
                    EnrollmentDate = DateTime.Parse("2010-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",
                    EnrollmentDate = DateTime.Parse("2011-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",
                    EnrollmentDate = DateTime.Parse("2005-09-01") }
            };

            foreach (Student s in students)
            {
                context.Student.Add(s);
            }
            context.SaveChanges();

            var instructors = new Instructor[]
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie",
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",
                    HireDate = DateTime.Parse("2004-02-12") }
            };

            foreach (Instructor i in instructors)
            {
                context.Instructors.Add(i);
            }
            context.SaveChanges();

            var departments = new Department[]
            {
                new Department { Name = "English",     Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
                new Department { Name = "Mathematics", Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
                new Department { Name = "Engineering", Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
                new Department { Name = "Economics",   Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
            };

            foreach (Department d in departments)
            {
                context.Departments.Add(d);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
            };

            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
                    Location = "Thompson 304" },
            };

            foreach (OfficeAssignment o in officeAssignments)
            {
                context.OfficeAssignments.Add(o);
            }
            context.SaveChanges();

            var courseInstructors = new CourseAssignment[]
            {
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
            };

            foreach (CourseAssignment ci in courseInstructors)
            {
                context.CourseAssignments.Add(ci);
            }
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    Grade = Grade.A
                },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    Grade = Grade.C
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B
                    },
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Li").ID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Justice").ID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B
                    }
            };

            foreach (Enrollment e in enrollments)
            {
                var enrollmentInDataBase = context.Enrollment.Where(
                    s =>
                            s.Student.ID == e.StudentID &&
                            s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollment.Add(e);
                }
            }
            context.SaveChanges();
        }
    }
}

O código anterior fornece dados de semente para as novas entidades. A maioria desse código cria novos objetos de entidade e carrega dados de exemplo. Os dados de exemplo são usados para teste. Consulte Enrollments e CourseAssignments para obter exemplos de como tabelas de junção muitos para muitos podem ser propagadas.

Adicionar uma migração

Compile o projeto.

Add-Migration ComplexDataModel

O comando anterior exibe um aviso sobre a possível perda de dados.

An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'

Se o comando database update é executado, o seguinte erro é produzido:

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.

Aplicar a migração

Agora que você tem um banco de dados existente, precisa pensar sobre como aplicar as alterações futuras a ele. Este tutorial mostra duas abordagens:

  • Remover e recriar o banco de dados
  • Aplicar a migração ao banco de dados existente. Embora esse método seja mais complexo e demorado, é a abordagem preferencial para ambientes de produção do mundo real. Observação: essa é uma seção opcional do tutorial. Você pode remover e recriar etapas e ignorar esta seção. Se você quiser seguir as etapas nesta seção, não realize as etapas de remover e recriar.

Remover e recriar o banco de dados

O código no DbInitializer atualizado adiciona dados de semente às novas entidades. Para forçar o EF Core a criar um novo BD, remova e atualize o BD:

No PMC (Console do Gerenciador de Pacotes), execute o seguinte comando:

Drop-Database
Update-Database

Execute Get-Help about_EntityFrameworkCore no PMC para obter informações de ajuda.

Execute o aplicativo. A execução do aplicativo executa o método DbInitializer.Initialize. O DbInitializer.Initialize popula o novo BD.

Abra o BD no SSOX:

  • Se o SSOX for aberto anteriormente, clique no botão Atualizar.
  • Expanda o nó Tabelas. As tabelas criadas são exibidas.

Tables in SSOX

Examine a tabela CourseAssignment:

  • Clique com o botão direito do mouse na tabela CourseAssignment e selecione Exibir Dados.
  • Verifique se a tabela CourseAssignment contém dados.

CourseAssignment data in SSOX

Aplicar a migração ao banco de dados existente

Esta seção é opcional. Estas etapas só funcionarão se você tiver ignorado a seção Remover e recriar o banco de dados anterior.

Quando as migrações são executadas com os dados existentes, pode haver restrições de FK que não são atendidas com os dados existentes. Com os dados de produção, é necessário executar etapas para migrar os dados existentes. Esta seção fornece um exemplo de correção de violações de restrição de FK. Não faça essas alterações de código sem um backup. Não faça essas alterações de código se você concluir a seção anterior e atualizou o banco de dados.

O arquivo {timestamp}_ComplexDataModel.cs contém o seguinte código:

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    type: "int",
    nullable: false,
    defaultValue: 0);

O código anterior adiciona uma FK DepartmentID que não permite valor nulo à tabela Course. O BD do tutorial anterior contém linhas em Course e, portanto, essa tabela não pode ser atualizada por migrações.

Para fazer a migração ComplexDataModel funcionar com os dados existentes:

  • Altere o código para dar à nova coluna (DepartmentID) um valor padrão.
  • Crie um departamento fictício chamado "Temp" para atuar como o departamento padrão.

Corrigir as restrições de chave estrangeira

Atualize o método Up das classes ComplexDataModel:

  • Abra o arquivo {timestamp}_ComplexDataModel.cs .
  • Comente a linha de código que adiciona a coluna DepartmentID à tabela Course.
migrationBuilder.AlterColumn<string>(
    name: "Title",
    table: "Course",
    maxLength: 50,
    nullable: true,
    oldClrType: typeof(string),
    oldNullable: true);
            
//migrationBuilder.AddColumn<int>(
//    name: "DepartmentID",
//    table: "Course",
//    nullable: false,
//    defaultValue: 0);

Adicione o código realçado a seguir. O novo código é inserido após o bloco .CreateTable( name: "Department":

migrationBuilder.CreateTable(
    name: "Department",
    columns: table => new
    {
        DepartmentID = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Budget = table.Column<decimal>(type: "money", nullable: false),
        InstructorID = table.Column<int>(type: "int", nullable: true),
        Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
        StartDate = table.Column<DateTime>(type: "datetime2", nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Department", x => x.DepartmentID);
        table.ForeignKey(
            name: "FK_Department_Instructor_InstructorID",
            column: x => x.InstructorID,
            principalTable: "Instructor",
            principalColumn: "ID",
            onDelete: ReferentialAction.Restrict);
    });

 migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    nullable: false,
    defaultValue: 1);

Com as alterações anteriores, as linhas Course existentes estarão relacionadas ao departamento "Temp" após a execução do método ComplexDataModelUp.

Um aplicativo de produção:

  • Inclui código ou scripts para adicionar linhas Department e linhas Course relacionadas às novas linhas Department.
  • Não usa o departamento "Temp" nem o valor padrão para Course.DepartmentID.

O próximo tutorial abrange os dados relacionados.

Recursos adicionais