Compartir a través de


Parte 5. Razor Pages con EF Core en ASP.NET Core: Modelo de datos

De Tom Dykstra, Jeremy Likness y Jon P Smith

En la aplicación web Contoso University se muestra cómo crear aplicaciones web Razor Pages con EF Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.

Si surgen problemas que no puede resolver, descargue la aplicación completada y compare ese código con el que ha creado siguiendo el tutorial.

En los tutoriales anteriores se trabajaba con un modelo de datos básico que se componía de tres entidades. En este tutorial:

  • Se agregan más entidades y relaciones.
  • Se personaliza el modelo de datos especificando el formato, la validación y las reglas de asignación de la base de datos.

El modelo de datos completo se muestra en la ilustración siguiente:

Diagrama de entidades

El siguiente diagrama de base de datos se realizó con Dataedo:

Diagrama de Dataedo

Para crear un diagrama de base de datos con Dataedo:

En el diagrama de Dataedo anterior, CourseInstructor es una tabla de unión creada por Entity Framework. Para más información, consulte Varios a varios.

La entidad Student

Reemplace el código de Models/Student.cs por esto:

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

En el código anterior se agrega una propiedad FullName y los atributos siguientes a las propiedades existentes:

La propiedad calculada FullName

FullName es una propiedad calculada que devuelve un valor que se crea mediante la concatenación de otras dos propiedades. No se puede establecer FullName, por lo que solo tiene un descriptor de acceso get. No se crea ninguna columna FullName en la base de datos.

El atributo DataType

[DataType(DataType.Date)]

Para las fechas de inscripción de alumnos, en todas las páginas se muestra actualmente la hora del día junto con la fecha, aunque solo es relevante la fecha. Mediante los atributos de anotación de datos, puede realizar un cambio de código que fijará el formato de presentación en todas las páginas en la que se muestren los datos.

El atributo DataType especifica un tipo de datos más específico que el tipo intrínseco de base de datos. En este caso solo se debe mostrar la fecha, no la fecha y hora. La enumeración DataType proporciona muchos tipos de datos, como Date (Fecha), Time (Hora), PhoneNumber (Número de teléfono), Currency (Divisa), EmailAddress (Dirección de correo electrónico), etc. El atributo DataType también puede permitir que la aplicación proporcione automáticamente características específicas del tipo. Por ejemplo:

  • El vínculo mailto: se crea automáticamente para DataType.EmailAddress.
  • El selector de fecha se proporciona para DataType.Date en la mayoría de los exploradores.

El atributo DataType emite atributos data- de HTML 5 (se pronuncia "datos dash"). Los atributos DataType no proporcionan validación.

El atributo DisplayFormat

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

DataType.Date no especifica el formato de la fecha que se muestra. De manera predeterminada, el campo de fecha se muestra según los formatos predeterminados basados en el elemento CultureInfo del servidor.

El atributo DisplayFormat se usa para especificar el formato de fecha de forma explícita. La configuración ApplyFormatInEditMode especifica que el formato también debe aplicarse a la interfaz de usuario de edición. Algunos campos no deben usar ApplyFormatInEditMode. Por ejemplo, el símbolo de divisa generalmente no debe mostrarse en un cuadro de texto de edición.

El atributo DisplayFormat puede usarse por sí solo. Normalmente se recomienda usar el atributo DataType con el atributo DisplayFormat. El atributo DataType transmite la semántica de los datos en lugar de cómo se representan en una pantalla. El atributo DataType proporciona las siguientes ventajas que no están disponibles en DisplayFormat:

  • El explorador puede habilitar características de HTML5. Por ejemplo, mostrar un control de calendario, el símbolo de divisa adecuado según la configuración regional, vínculos de correo electrónico y validación de entradas del lado cliente.
  • De manera predeterminada, el explorador representa los datos con el formato correcto según la configuración regional.

Para obtener más información, vea la documentación del asistente de etiquetas <entrada>.

El atributo StringLength

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

Las reglas de validación de datos y los mensajes de error de validación se pueden especificar con atributos. El atributo StringLength especifica la longitud mínima y máxima de caracteres que se permite en un campo de datos. En el código que se muestra se limitan los nombres a un máximo de 50 caracteres. Más adelante se muestra un ejemplo en el que se establece la longitud mínima de la cadena.

El atributo StringLength también proporciona validación del lado cliente y del lado servidor. El valor mínimo no influye en el esquema de base de datos.

El atributo StringLength no impide que un usuario escriba un espacio en blanco para un nombre. El atributo RegularExpression se puede usar para aplicar restricciones a la entrada. Por ejemplo, el código siguiente requiere que el primer carácter sea una letra mayúscula y el resto de caracteres sean alfabéticos:

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

En el Explorador de objetos de SQL Server, (SSOX) abra el diseñador de tablas de Student haciendo doble clic en la tabla Student.

Tabla de estudiantes en SSOX antes de las migraciones

La imagen anterior muestra el esquema para la tabla Student. Los campos de nombre tienen el tipo nvarchar(MAX). Cuando más adelante en este tutorial se cree y se aplique una migración, los campos de nombre se convierten en nvarchar(50) como resultado de los atributos de longitud de cadena.

El atributo Column

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

Los atributos pueden controlar cómo se asignan las clases y propiedades a la base de datos. En el modelo Student, el atributo Column se usa para asignar el nombre de la propiedad FirstMidName a "FirstName" en la base de datos.

Cuando se crea la base de datos, los nombres de propiedad en el modelo se usan para los nombres de columna (excepto cuando se usa el atributo Column). El modelo Student usa FirstMidName para el nombre de campo por la posibilidad de que el campo contenga también un segundo nombre.

Con el atributo [Column], Student.FirstMidName en el modelo de datos se asigna a la columna FirstName de la tabla Student. La adición del atributo Column cambia el modelo de respaldo de SchoolContext. El modelo que está haciendo la copia de seguridad de SchoolContext ya no coincide con la base de datos. Más adelante en este tutorial se agregará una migración para resolver esa discrepancia.

El atributo Required

[Required]

El atributo Required hace que las propiedades de nombre sean campos obligatorios. El atributo Required no es necesario para los tipos que no aceptan valores NULL, como los tipos de valor (por ejemplo DateTime, int y double). Los tipos que no aceptan valores NULL se tratan automáticamente como campos obligatorios.

El atributo Required se debe usar con MinimumLength para que se aplique MinimumLength.

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

MinimumLength y Required permiten que el espacio en blanco satisfaga la validación. Utilice el atributo RegularExpression para el control total sobre la cadena.

El atributo Display

[Display(Name = "Last Name")]

El atributo Display especifica que el título de los cuadros de texto debe ser "First Name" (Nombre), "Last Name" (Apellidos), "Full Name" (Nombre completo) y "Enrollment Date" (Fecha de inscripción). El título predeterminado no tiene espacios dividiendo las palabras, por ejemplo, "Nombrecompleto".

Crear una migración

Ejecute la aplicación y vaya a la página Students. Se inicia una excepción. El atributo [Column] hace que EF espere encontrar una columna denominada FirstName, pero el nombre de la columna en la base de datos sigue siendo FirstMidName.

El mensaje de error es similar al ejemplo siguiente:

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

SchoolContext
  • En la Consola del administrador de paquetes, escriba los comandos siguientes para crear una migración y actualizar la base de datos:

    Add-Migration ColumnFirstName
    Update-Database
    
    

    El primero de estos comandos genera el siguiente mensaje de advertencia:

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

    La advertencia se genera porque los campos de nombre ahora están limitados a 50 caracteres. Si un nombre en la base de datos tenía más de 50 caracteres, se perderían desde el 51 hasta el último carácter.

  • Abra la tabla de estudiantes en SSOX:

    Tabla de estudiantes en SSOX después de las migraciones

    Antes de aplicar la migración, las columnas de nombre eran de tipo nvarchar(MAX). Las columnas de nombre ahora son nvarchar(50). El nombre de columna ha cambiado de FirstMidName a FirstName.

  • Ejecute la aplicación y vaya a la página Students.
  • Observe que las horas no se escriben ni se muestran junto con las fechas.
  • Seleccione Crear nuevo e intente escribir un nombre de más de 50 caracteres.

Nota

En las secciones siguientes, la creación de la aplicación en algunas de las fases genera errores del compilador. Las instrucciones especifican cuándo se debe compilar la aplicación.

La entidad Instructor

Cree el archivo Models/Instructor.cs con el siguiente 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; }
    }
}

En una sola línea puede haber varios atributos. Los atributos HireDate pudieron escribirse de la manera siguiente:

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

Courses y OfficeAssignment son propiedades de navegación.

Un instructor puede impartir cualquier número de cursos, por lo que Courses se define como una colección.

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

Un instructor puede tener como máximo una oficina, por lo que la propiedad OfficeAssignment contiene una sola entidad OfficeAssignment. OfficeAssignment es NULL si no se asigna ninguna oficina.

public OfficeAssignment OfficeAssignment { get; set; }

La entidad OfficeAssignment

Entidad OfficeAssignment

Cree el archivo Models/OfficeAssignment.cs con el siguiente 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; }
    }
}

El atributo Key

El atributo [Key] se usa para identificar una propiedad como clave principal (PK) cuando el nombre de la propiedad es diferente de classnameID o ID.

Hay una relación de uno a cero o uno entre las entidades Instructor y OfficeAssignment. Solo existe una asignación de oficina en relación con el instructor a la que está asignada. La clave principal de OfficeAssignment también es la clave externa (FK) para la entidad Instructor. Una relación de uno a cero o uno se produce cuando una clave principal de una tabla es a la vez una clave principal y una clave externa de otra tabla.

EF Core no puede reconocer automáticamente InstructorID como la clave principal de OfficeAssignment porque InstructorID no sigue la convención de nomenclatura de ID o classnameID. Por tanto, se usa el atributo Key para identificar InstructorID como la clave principal:

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

De forma predeterminada, EF Core trata la clave como no generada por la base de datos porque la columna es para una relación de identificación. Para más información, consulte Claves de EF.

La propiedad de navegación Instructor

La propiedad de navegación Instructor.OfficeAssignment puede ser NULL porque es posible que no haya una fila OfficeAssignment para un instructor determinado. Un instructor podría no tener una asignación de oficina.

La propiedad de navegación OfficeAssignment.Instructor siempre tendrá una entidad de instructor porque el tipo InstructorID de clave externa es int, un tipo de valor que no acepta valores NULL. Una asignación de oficina no puede existir sin un instructor.

Cuando una entidad Instructor tiene una entidad OfficeAssignment relacionada, cada entidad tiene una referencia a la otra en su propiedad de navegación.

La entidad Course

Actualice Models/Course.cs con el siguiente 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; }
    }
}

La entidad Course tiene una propiedad de clave externa (FK) DepartmentID. DepartmentID apunta a la entidad relacionada Department. La entidad Course tiene una propiedad de navegación Department.

EF Core no requiere una propiedad de clave externa para un modelo de datos cuando el modelo tiene una propiedad de navegación para una entidad relacionada. EF Core crea automáticamente claves externas en la base de datos siempre que se necesiten. EF Core crea propiedades paralelas para las claves externas creadas automáticamente. Pero la inclusión explícita de la clave externa en el modelo de datos puede hacer que las actualizaciones sean más sencillas y eficaces. Por ejemplo, considere la posibilidad de un modelo donde la propiedad de la clave externa DepartmentIDno está incluida. Cuando se captura una entidad de curso para editar:

  • La propiedad Department es null si no se carga de forma explícita.
  • Para actualizar la entidad Course, la entidad Department debe capturarse en primer lugar.

Cuando se incluye la propiedad de clave externa DepartmentID en el modelo de datos, no es necesario capturar la entidad Department antes de una actualización.

El atributo DatabaseGenerated

El atributo [DatabaseGenerated(DatabaseGeneratedOption.None)] especifica que la aplicación proporciona la clave principal, en lugar de generarla la base de datos.

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

De forma predeterminada, EF Core asume que la base de datos genera valores de clave principal. La generación por parte de la base de datos suele ser el mejor enfoque. Para las entidades Course, el usuario especifica la clave principal. Por ejemplo, un número de curso como una serie de 1000 para el departamento de matemáticas, una serie de 2000 para el departamento de inglés.

También se puede usar el atributo DatabaseGenerated para generar valores predeterminados. Por ejemplo, la base de datos puede generar de forma automática un campo de fecha para registrar la fecha en que se crea o actualiza una fila. Para obtener más información, vea Propiedades generadas.

Propiedades de clave externa y de navegación

Las propiedades de clave externa (FK) y las de navegación de la entidad Course reflejan las relaciones siguientes:

Un curso se asigna a un departamento, por lo que hay una clave externa DepartmentID y una propiedad de navegación Department.

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

Un curso puede tener cualquier número de alumnos inscritos en él, por lo que la propiedad de navegación Enrollments es una colección:

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

Un curso puede ser impartido por varios instructores, por lo que la propiedad de navegación Instructors es una colección:

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

La entidad Department

Cree el archivo Models/Department.cs con el siguiente 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; }
    }
}

El atributo Column

Anteriormente se usó el atributo Column para cambiar la asignación de nombres de columna. En el código de la entidad Department, se usó el atributo Column para cambiar la asignación de tipos de datos de SQL. La columna Budget se define mediante el tipo money de SQL Server en la base de datos:

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

Por lo general, la asignación de columnas no es necesaria. EF Core elige el tipo de datos de SQL Server apropiado en función del tipo CLR para la propiedad. El tipo CLR decimal se asigna a un tipo decimal de SQL Server. Budget es para la divisa, y el tipo de datos money es más adecuado para la divisa.

Propiedades de clave externa y de navegación

Las propiedades de clave externa y de navegación reflejan las relaciones siguientes:

  • Un departamento puede tener o no un administrador.
  • Un administrador siempre es un instructor. Por lo tanto, la propiedad InstructorID se incluye como la clave externa para la entidad Instructor.

La propiedad de navegación se denomina Administrator pero contiene una entidad Instructor:

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

El signo de interrogación ? en el código anterior especifica que la propiedad acepta valores NULL.

Un departamento puede tener varios cursos, por lo que hay una propiedad de navegación Courses:

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

Por convención, EF Core permite la eliminación en cascada de las claves externas que no acepten valores NULL ni relaciones de varios a varios. Este comportamiento predeterminado puede dar lugar a reglas de eliminación en cascada circular. Las reglas de eliminación en cascada circular inician una excepción cuando se agrega una migración.

Por ejemplo, si la propiedad Department.InstructorID se ha definido como que no acepta valores NULL, EF Core configurará una regla de eliminación en cascada. En ese caso, el departamento se eliminará cuando se elimine el instructor asignado como administrador. En este escenario, una regla de restricción tendría más sentido. La API fluida siguiente establecería una regla de restricción y deshabilitaría la eliminación en cascada.

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

Propiedades de clave externa y navegación de Enrollment

Un registro de inscripción corresponde a un curso realizado por un alumno.

La entidad Enrollment

Actualice Models/Enrollment.cs con el siguiente 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; }
    }
}

Las propiedades de clave externa y de navegación reflejan las relaciones siguientes:

Un registro de inscripción es para un curso, por lo que hay una propiedad de clave externa CourseID y una propiedad de navegación Course:

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

Un registro de inscripción es para un alumno, por lo que hay una propiedad de clave externa StudentID y una propiedad de navegación Student:

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

Relaciones Varios a Varios

Hay una relación de varios a varios entre las entidades Student y Course. La entidad Enrollment funciona como una tabla de unión de varios a varios con carga útil en la base de datos. Con carga útil significa que la tabla Enrollment contiene datos adicionales, además de claves principales para las tablas unidas. En la entidad Enrollment, los datos adicionales además de las claves externas son la clave principal y Grade.

En la ilustración siguiente se muestra el aspecto de estas relaciones en un diagrama de entidades. (Este diagrama se generó con EF Power Tools para EF 6.x. La creación del diagrama no forma parte del tutorial).

Relación de varios a varios entre estudiantes y cursos

Cada línea de relación tiene un 1 en un extremo y un asterisco (*) en el otro, para indicar una relación uno a varios.

Si la tabla Enrollment no incluyera información de calificaciones, solo tendría que contener las dos claves externas (CourseID y StudentID). Una tabla combinada de varios a varios sin carga útil se suele denominar una tabla combinada pura (PJT).

Las entidades Instructor y Course tienen una relación de varios a varios con una tabla de unión pura.

Actualizar el contexto de base de datos

Actualice Data/SchoolContext.cs con el siguiente 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));
        }
    }
}

El código anterior agrega las nuevas entidades y configura la relación de varios a varios entre las entidades Instructor y Course.

Alternativa de la API fluida a los atributos

El método OnModelCreating del código anterior usa la API fluida para configurar el comportamiento de EF Core. La API se denomina "fluida" porque a menudo se usa para encadenar una serie de llamadas de método en una única instrucción. El código siguiente es un ejemplo de la API fluida:

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

En este tutorial, la API fluida solo se usa para la asignación de base de datos que no se puede realizar con atributos. Pero la API fluida puede especificar casi todas las reglas de formato, validación y asignación que se pueden realizar mediante el uso de atributos.

Algunos atributos como MinimumLength no se pueden aplicar con la API fluida. MinimumLength no cambia el esquema, solo aplica una regla de validación de longitud mínima.

Algunos desarrolladores prefieren usar la API fluida exclusivamente para mantener limpias sus clases de entidad. Se pueden mezclar atributos y la API fluida. Hay algunas configuraciones que solo se pueden realizar con la API fluida, por ejemplo, especificar una clave principal compuesta. Hay algunas configuraciones que solo se pueden realizar con atributos (MinimumLength). La práctica recomendada para el uso de atributos o API fluida:

  • Elija uno de estos dos enfoques.
  • Use el enfoque elegido de forma tan coherente como sea posible.

Algunos de los atributos utilizados en este tutorial se usan para:

  • Solo validación (por ejemplo, MinimumLength).
  • Solo configuración de EF Core (por ejemplo, HasKey).
  • Validación y configuración de EF Core (por ejemplo, [StringLength(50)]).

Para obtener más información sobre la diferencia entre los atributos y la API fluida, vea Métodos de configuración.

Inicializar la base de datos

Actualice el código de 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();
        }
    }
}

El código anterior proporciona datos de inicialización para las nuevas entidades. La mayor parte de este código crea objetos de entidad y carga los datos de ejemplo. Los datos de ejemplo se usan para pruebas.

Aplicar la migración o quitar y volver a crear

Con la base de datos existente, existen dos métodos para cambiar la base de datos:

Ambas opciones funcionan para SQL Server. Aunque el método de aplicación de la migración es más complejo y lento, es el enfoque preferido para entornos de producción del mundo real.

Quitar y volver a crear la base de datos

Para obligar a EF Core a crear una base de datos, quite y actualice la base de datos:

  • Elimine la carpeta Migrations.
  • En la Consola del Administrador de paquetes (PMC), ejecute los siguientes comandos:
Drop-Database
Add-Migration InitialCreate
Update-Database

Ejecutar la aplicación. Ejecutar la aplicación ejecuta el método DbInitializer.Initialize. DbInitializer.Initialize rellena la base de datos nueva.

Abra la base de datos en SSOX:

  • Si anteriormente se abrió SSOX, haga clic en el botón Actualizar.
  • Expanda el nodo Tablas. Se muestran las tablas creadas.

Pasos siguientes

En los dos tutoriales siguientes se muestra cómo leer y actualizar los datos relacionados.

En los tutoriales anteriores se trabajaba con un modelo de datos básico que se componía de tres entidades. En este tutorial:

  • Se agregan más entidades y relaciones.
  • Se personaliza el modelo de datos especificando el formato, la validación y las reglas de asignación de la base de datos.

El modelo de datos completo se muestra en la ilustración siguiente:

Diagrama de entidades

La entidad Student

Entidad Student

Reemplace el código de Models/Student.cs por esto:

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

En el código anterior se agrega una propiedad FullName y los atributos siguientes a las propiedades existentes:

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

La propiedad calculada FullName

FullName es una propiedad calculada que devuelve un valor que se crea mediante la concatenación de otras dos propiedades. No se puede establecer FullName, por lo que solo tiene un descriptor de acceso get. No se crea ninguna columna FullName en la base de datos.

El atributo DataType

[DataType(DataType.Date)]

Para las fechas de inscripción de alumnos, en todas las páginas se muestra actualmente la hora del día junto con la fecha, aunque solo es relevante la fecha. Mediante los atributos de anotación de datos, puede realizar un cambio de código que fijará el formato de presentación en todas las páginas en la que se muestren los datos.

El atributo DataType especifica un tipo de datos más específico que el tipo intrínseco de base de datos. En este caso solo se debe mostrar la fecha, no la fecha y hora. La enumeración DataType proporciona muchos tipos de datos, como Date (Fecha), Time (Hora), PhoneNumber (Número de teléfono), Currency (Divisa), EmailAddress (Dirección de correo electrónico), etc. El atributo DataType también puede permitir que la aplicación proporcione automáticamente características específicas del tipo. Por ejemplo:

  • El vínculo mailto: se crea automáticamente para DataType.EmailAddress.
  • El selector de fecha se proporciona para DataType.Date en la mayoría de los exploradores.

El atributo DataType emite atributos data- de HTML 5 (se pronuncia "datos dash"). Los atributos DataType no proporcionan validación.

El atributo DisplayFormat

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

DataType.Date no especifica el formato de la fecha que se muestra. De manera predeterminada, el campo de fecha se muestra según los formatos predeterminados basados en el elemento CultureInfo del servidor.

El atributo DisplayFormat se usa para especificar el formato de fecha de forma explícita. La configuración ApplyFormatInEditMode especifica que el formato también debe aplicarse a la interfaz de usuario de edición. Algunos campos no deben usar ApplyFormatInEditMode. Por ejemplo, el símbolo de divisa generalmente no debe mostrarse en un cuadro de texto de edición.

El atributo DisplayFormat puede usarse por sí solo. Normalmente se recomienda usar el atributo DataType con el atributo DisplayFormat. El atributo DataType transmite la semántica de los datos en lugar de cómo se representan en una pantalla. El atributo DataType proporciona las siguientes ventajas que no están disponibles en DisplayFormat:

  • El explorador puede habilitar características de HTML5. Por ejemplo, mostrar un control de calendario, el símbolo de divisa adecuado según la configuración regional, vínculos de correo electrónico y validación de entradas del lado cliente.
  • De manera predeterminada, el explorador representa los datos con el formato correcto según la configuración regional.

Para obtener más información, vea la documentación del asistente de etiquetas <entrada>.

El atributo StringLength

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

Las reglas de validación de datos y los mensajes de error de validación se pueden especificar con atributos. El atributo StringLength especifica la longitud mínima y máxima de caracteres que se permite en un campo de datos. En el código que se muestra se limitan los nombres a un máximo de 50 caracteres. Más adelante se muestra un ejemplo en el que se establece la longitud mínima de la cadena.

El atributo StringLength también proporciona validación del lado cliente y del lado servidor. El valor mínimo no influye en el esquema de base de datos.

El atributo StringLength no impide que un usuario escriba un espacio en blanco para un nombre. El atributo RegularExpression se puede usar para aplicar restricciones a la entrada. Por ejemplo, el código siguiente requiere que el primer carácter sea una letra mayúscula y el resto de caracteres sean alfabéticos:

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

En el Explorador de objetos de SQL Server, (SSOX) abra el diseñador de tablas de Student haciendo doble clic en la tabla Student.

Tabla de estudiantes en SSOX antes de las migraciones

La imagen anterior muestra el esquema para la tabla Student. Los campos de nombre tienen el tipo nvarchar(MAX). Cuando más adelante en este tutorial se cree y se aplique una migración, los campos de nombre se convierten en nvarchar(50) como resultado de los atributos de longitud de cadena.

El atributo Column

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

Los atributos pueden controlar cómo se asignan las clases y propiedades a la base de datos. En el modelo Student, el atributo Column se usa para asignar el nombre de la propiedad FirstMidName a "FirstName" en la base de datos.

Cuando se crea la base de datos, los nombres de propiedad en el modelo se usan para los nombres de columna (excepto cuando se usa el atributo Column). El modelo Student usa FirstMidName para el nombre de campo por la posibilidad de que el campo contenga también un segundo nombre.

Con el atributo [Column], Student.FirstMidName en el modelo de datos se asigna a la columna FirstName de la tabla Student. La adición del atributo Column cambia el modelo de respaldo de SchoolContext. El modelo que está haciendo la copia de seguridad de SchoolContext ya no coincide con la base de datos. Más adelante en este tutorial se agregará una migración para resolver esa discrepancia.

El atributo Required

[Required]

El atributo Required hace que las propiedades de nombre sean campos obligatorios. El atributo Required no es necesario para los tipos que no aceptan valores NULL, como los tipos de valor (por ejemplo DateTime, int y double). Los tipos que no aceptan valores NULL se tratan automáticamente como campos obligatorios.

El atributo Required se debe usar con MinimumLength para que se aplique MinimumLength.

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

MinimumLength y Required permiten que el espacio en blanco satisfaga la validación. Utilice el atributo RegularExpression para el control total sobre la cadena.

El atributo Display

[Display(Name = "Last Name")]

El atributo Display especifica que el título de los cuadros de texto debe ser "First Name" (Nombre), "Last Name" (Apellidos), "Full Name" (Nombre completo) y "Enrollment Date" (Fecha de inscripción). El título predeterminado no tiene espacios dividiendo las palabras, por ejemplo, "Nombrecompleto".

Crear una migración

Ejecute la aplicación y vaya a la página Students. Se inicia una excepción. El atributo [Column] hace que EF espere encontrar una columna denominada FirstName, pero el nombre de la columna en la base de datos sigue siendo FirstMidName.

El mensaje de error es similar al ejemplo siguiente:

SqlException: Invalid column name 'FirstName'.
  • En la Consola del administrador de paquetes, escriba los comandos siguientes para crear una migración y actualizar la base de datos:

    Add-Migration ColumnFirstName
    Update-Database
    

    El primero de estos comandos genera el siguiente mensaje de advertencia:

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

    La advertencia se genera porque los campos de nombre ahora están limitados a 50 caracteres. Si un nombre en la base de datos tenía más de 50 caracteres, se perderían desde el 51 hasta el último carácter.

  • Abra la tabla de estudiantes en SSOX:

    Tabla de estudiantes en SSOX después de las migraciones

    Antes de aplicar la migración, las columnas de nombre eran de tipo nvarchar(MAX). Las columnas de nombre ahora son nvarchar(50). El nombre de columna ha cambiado de FirstMidName a FirstName.

  • Ejecute la aplicación y vaya a la página Students.
  • Observe que las horas no se escriben ni se muestran junto con las fechas.
  • Seleccione Crear nuevo e intente escribir un nombre de más de 50 caracteres.

Nota

En las secciones siguientes, la creación de la aplicación en algunas de las fases genera errores del compilador. Las instrucciones especifican cuándo se debe compilar la aplicación.

La entidad Instructor

La entidad Instructor

Cree el archivo Models/Instructor.cs con el siguiente 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; }
    }
}

En una sola línea puede haber varios atributos. Los atributos HireDate pudieron escribirse de la manera siguiente:

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

CourseAssignments y OfficeAssignment son propiedades de navegación.

Un instructor puede impartir cualquier número de cursos, por lo que CourseAssignments se define como una colección.

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

Un instructor puede tener como máximo una oficina, por lo que la propiedad OfficeAssignment contiene una sola entidad OfficeAssignment. OfficeAssignment es NULL si no se asigna ninguna oficina.

public OfficeAssignment OfficeAssignment { get; set; }

La entidad OfficeAssignment

Entidad OfficeAssignment

Cree el archivo Models/OfficeAssignment.cs con el siguiente 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; }
    }
}

El atributo Key

El atributo [Key] se usa para identificar una propiedad como la clave principal (PK) cuando el nombre de propiedad es diferente de classnameID o ID.

Hay una relación de uno a cero o uno entre las entidades Instructor y OfficeAssignment. Solo existe una asignación de oficina en relación con el instructor a la que está asignada. La clave principal de OfficeAssignment también es la clave externa (FK) para la entidad Instructor.

EF Core no puede reconocer automáticamente InstructorID como la clave principal de OfficeAssignment porque InstructorID no sigue la convención de nomenclatura de ID o classnameID. Por tanto, se usa el atributo Key para identificar InstructorID como la clave principal:

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

De forma predeterminada, EF Core trata la clave como no generada por la base de datos porque la columna es para una relación de identificación.

La propiedad de navegación Instructor

La propiedad de navegación Instructor.OfficeAssignment puede ser NULL porque es posible que no haya una fila OfficeAssignment para un instructor determinado. Un instructor podría no tener una asignación de oficina.

La propiedad de navegación OfficeAssignment.Instructor siempre tendrá una entidad de instructor porque el tipo InstructorID de clave externa es int, un tipo de valor que no acepta valores NULL. Una asignación de oficina no puede existir sin un instructor.

Cuando una entidad Instructor tiene una entidad OfficeAssignment relacionada, cada entidad tiene una referencia a la otra en su propiedad de navegación.

La entidad Course

La entidad Course

Actualice Models/Course.cs con el siguiente 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; }
    }
}

La entidad Course tiene una propiedad de clave externa (FK) DepartmentID. DepartmentID apunta a la entidad relacionada Department. La entidad Course tiene una propiedad de navegación Department.

EF Core no requiere una propiedad de clave externa para un modelo de datos cuando el modelo tiene una propiedad de navegación para una entidad relacionada. EF Core crea automáticamente claves externas en la base de datos siempre que se necesiten. EF Core crea propiedades paralelas para las claves externas creadas automáticamente. Pero la inclusión explícita de la clave externa en el modelo de datos puede hacer que las actualizaciones sean más sencillas y eficaces. Por ejemplo, considere la posibilidad de un modelo donde la propiedad de la clave externa DepartmentIDno está incluida. Cuando se captura una entidad de curso para editar:

  • La propiedad Department es NULL si no se carga de forma explícita.
  • Para actualizar la entidad Course, la entidad Department debe capturarse en primer lugar.

Cuando se incluye la propiedad de clave externa DepartmentID en el modelo de datos, no es necesario capturar la entidad Department antes de una actualización.

El atributo DatabaseGenerated

El atributo [DatabaseGenerated(DatabaseGeneratedOption.None)] especifica que la aplicación proporciona la clave principal, en lugar de generarla la base de datos.

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

De forma predeterminada, EF Core asume que la base de datos genera valores de clave principal. La generación por parte de la base de datos suele ser el mejor enfoque. Para las entidades Course, el usuario especifica la clave principal. Por ejemplo, un número de curso como una serie de 1000 para el departamento de matemáticas, una serie de 2000 para el departamento de inglés.

También se puede usar el atributo DatabaseGenerated para generar valores predeterminados. Por ejemplo, la base de datos puede generar de forma automática un campo de fecha para registrar la fecha en que se crea o actualiza una fila. Para obtener más información, vea Propiedades generadas.

Propiedades de clave externa y de navegación

Las propiedades de clave externa (FK) y las de navegación de la entidad Course reflejan las relaciones siguientes:

Un curso se asigna a un departamento, por lo que hay una clave externa DepartmentID y una propiedad de navegación Department.

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

Un curso puede tener cualquier número de alumnos inscritos en él, por lo que la propiedad de navegación Enrollments es una colección:

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

Un curso puede ser impartido por varios instructores, por lo que la propiedad de navegación CourseAssignments es una colección:

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

CourseAssignment se explica más adelante.

La entidad Department

La entidad Department

Cree el archivo Models/Department.cs con el siguiente 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; }
    }
}

El atributo Column

Anteriormente se usó el atributo Column para cambiar la asignación de nombres de columna. En el código de la entidad Department, se usó el atributo Column para cambiar la asignación de tipos de datos de SQL. La columna Budget se define mediante el tipo money de SQL Server en la base de datos:

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

Por lo general, la asignación de columnas no es necesaria. EF Core elige el tipo de datos de SQL Server apropiado en función del tipo CLR para la propiedad. El tipo CLR decimal se asigna a un tipo decimal de SQL Server. Budget es para la divisa, y el tipo de datos money es más adecuado para la divisa.

Propiedades de clave externa y de navegación

Las propiedades de clave externa y de navegación reflejan las relaciones siguientes:

  • Un departamento puede tener o no un administrador.
  • Un administrador siempre es un instructor. Por lo tanto, la propiedad InstructorID se incluye como la clave externa para la entidad Instructor.

La propiedad de navegación se denomina Administrator pero contiene una entidad Instructor:

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

El signo de interrogación (?) en el código anterior especifica que la propiedad acepta valores NULL.

Un departamento puede tener varios cursos, por lo que hay una propiedad de navegación Courses:

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

Por convención, EF Core permite la eliminación en cascada de las claves externas que no acepten valores NULL ni relaciones de varios a varios. Este comportamiento predeterminado puede dar lugar a reglas de eliminación en cascada circular. Las reglas de eliminación en cascada circular inician una excepción cuando se agrega una migración.

Por ejemplo, si la propiedad Department.InstructorID se ha definido como que no acepta valores NULL, EF Core configurará una regla de eliminación en cascada. En ese caso, el departamento se eliminará cuando se elimine el instructor asignado como administrador. En este escenario, una regla de restricción tendría más sentido. La API fluida siguiente establecería una regla de restricción y deshabilitaría la eliminación en cascada.

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

La entidad Enrollment

Un registro de inscripción corresponde a un curso realizado por un alumno.

La entidad Enrollment

Actualice Models/Enrollment.cs con el siguiente 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; }
    }
}

Propiedades de clave externa y de navegación

Las propiedades de clave externa y de navegación reflejan las relaciones siguientes:

Un registro de inscripción es para un curso, por lo que hay una propiedad de clave externa CourseID y una propiedad de navegación Course:

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

Un registro de inscripción es para un alumno, por lo que hay una propiedad de clave externa StudentID y una propiedad de navegación Student:

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

Relaciones Varios a Varios

Hay una relación de varios a varios entre las entidades Student y Course. La entidad Enrollment funciona como una tabla combinada varios a varios con carga útil en la base de datos. "Con carga útil" significa que la tabla Enrollment contiene datos adicionales, además de claves externas de las tablas combinadas (en este caso, la clave principal y Grade).

En la ilustración siguiente se muestra el aspecto de estas relaciones en un diagrama de entidades. (Este diagrama se generó con EF Power Tools para EF 6.x. La creación del diagrama no forma parte del tutorial).

Relación de varios a varios entre estudiantes y cursos

Cada línea de relación tiene un 1 en un extremo y un asterisco (*) en el otro, para indicar una relación uno a varios.

Si la tabla Enrollment no incluyera información de calificaciones, solo tendría que contener las dos claves externas (CourseID y StudentID). Una tabla combinada de varios a varios sin carga útil se suele denominar una tabla combinada pura (PJT).

Las entidades Instructor y Course tienen una relación de varios a varios con una tabla combinada pura.

Nota: EF 6.x es compatible con las tablas de combinación implícitas para relaciones de varios a varios, pero EF Core no. Para obtener más información, consulte Relaciones de varios a varios en EF Core 2.0.

La entidad CourseAssignment

La entidad CourseAssignment

Cree el archivo Models/CourseAssignment.cs con el siguiente 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; }
    }
}

La relación de varios a varios entre instructores y cursos requiere una tabla de combinación y la entidad para esa tabla de combinación es CourseAssignment.

Relación de varios a varios Instructor-to-Courses

Es común denominar una entidad de combinación EntityName1EntityName2. Por ejemplo, la tabla de combinación Instructor-to-Courses con este patrón sería CourseInstructor. Pero se recomienda usar un nombre que describa la relación.

Los modelos de datos empiezan de manera sencilla y crecen. Las tablas combinadas sin carga útil (PJT) evolucionan con frecuencia para incluir la carga útil. A partir de un nombre de entidad descriptivo, no es necesario cambiar el nombre cuando la tabla combinada cambia. Idealmente, la entidad de combinación tendrá su propio nombre natural (posiblemente una sola palabra) en el dominio de empresa. Por ejemplo, Books y Customers podrían vincularse a través de una entidad combinada denominada Ratings. Para la relación de varios a varios Instructor-to-Courses, se prefiere CourseAssignment a CourseInstructor.

Clave compuesta

Las dos claves externas en CourseAssignment (InstructorID y CourseID) juntas identifican de forma única cada fila de la tabla CourseAssignment. CourseAssignment no requiere una clave principal dedicada. Las propiedades InstructorID y CourseID funcionan como una clave principal compuesta. La única manera de especificar claves principales compuestas en EF Core es con la API fluida. La sección siguiente muestra cómo configurar la clave principal compuesta.

La clave compuesta asegura que:

  • Se permiten varias filas para un curso.
  • Se permiten varias filas para un instructor.
  • No se permiten varias filas para el mismo instructor y curso.

La entidad de combinación Enrollment define su propia clave principal, por lo que este tipo de duplicados son posibles. Para evitar los duplicados:

  • Agregue un índice único en los campos de clave externa, o
  • Configure Enrollment con una clave compuesta principal similar a CourseAssignment. Para obtener más información, vea Índices.

Actualizar el contexto de base de datos

Actualice Data/SchoolContext.cs con el siguiente 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 });
        }
    }
}

El código anterior agrega las nuevas entidades y configura la clave principal compuesta de la entidad CourseAssignment.

Alternativa de la API fluida a los atributos

El método OnModelCreating del código anterior usa la API fluida para configurar el comportamiento de EF Core. La API se denomina "fluida" porque a menudo se usa para encadenar una serie de llamadas de método en una única instrucción. El código siguiente es un ejemplo de la API fluida:

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

En este tutorial, la API fluida solo se usa para la asignación de base de datos que no se puede realizar con atributos. Pero la API fluida puede especificar casi todas las reglas de formato, validación y asignación que se pueden realizar mediante el uso de atributos.

Algunos atributos como MinimumLength no se pueden aplicar con la API fluida. MinimumLength no cambia el esquema, solo aplica una regla de validación de longitud mínima.

Algunos desarrolladores prefieren usar la API fluida exclusivamente para mantener "limpias" sus clases de entidad. Se pueden combinar los atributos y la API fluida. Hay algunas configuraciones que solo se pueden realizar con la API fluida (especificando una clave principal compuesta). Hay algunas configuraciones que solo se pueden realizar con atributos (MinimumLength). La práctica recomendada para el uso de atributos o API fluida:

  • Elija uno de estos dos enfoques.
  • Use el enfoque elegido de forma tan coherente como sea posible.

Algunos de los atributos utilizados en este tutorial se usan para:

  • Solo validación (por ejemplo, MinimumLength).
  • Solo configuración de EF Core (por ejemplo, HasKey).
  • Validación y configuración de EF Core (por ejemplo, [StringLength(50)]).

Para obtener más información sobre la diferencia entre los atributos y la API fluida, vea Métodos de configuración.

Diagrama de entidades

En la siguiente ilustración se muestra el diagrama creado por EF Power Tools para el modelo School completado.

Diagrama de entidades

El diagrama anterior muestra:

  • Varias líneas de relación uno a varios (1 a *).
  • La línea de relación de uno a cero o uno (1 a 0..1) entre las entidades Instructor y OfficeAssignment.
  • La línea de relación de cero o uno o varios (0..1 a *) entre las entidades Instructor y Department.

Inicializar la base de datos

Actualice el código de 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();
        }
    }
}

El código anterior proporciona datos de inicialización para las nuevas entidades. La mayor parte de este código crea objetos de entidad y carga los datos de ejemplo. Los datos de ejemplo se usan para pruebas. Consulte Enrollments y CourseAssignments para obtener ejemplos de cómo pueden inicializarse las tablas de combinación de varios a varios.

Agregar una migración

Compile el proyecto.

En la Consola del administrador de paquetes, ejecute el comando siguiente.

Add-Migration ComplexDataModel

El comando anterior muestra una advertencia sobre la posible pérdida de datos.

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'

Si se ejecuta el comando database update, se genera el error siguiente:

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'.

En la sección siguiente, verá qué puede hacer con respecto a este error.

Aplicar la migración o quitar y volver a crear

Ahora que tiene una base de datos existente, debe pensar en cómo aplicarle cambios. En este tutorial se muestran dos alternativas:

Ambas opciones funcionan para SQL Server. Aunque el método de aplicación de la migración es más complejo y lento, es el enfoque preferido para entornos de producción del mundo real.

Quitar y volver a crear la base de datos

Omita esta sección si usa SQL Server y quiere realizar el enfoque de aplicación de la migración en la sección siguiente.

Para obligar a EF Core a crear una base de datos, quite y actualice la base de datos:

  • En la Consola del Administrador de paquetes (PMC), ejecute el comando siguiente:

    Drop-Database
    
  • Elimine la carpeta Migrations y, después, ejecute el comando siguiente:

    Add-Migration InitialCreate
    Update-Database
    

Ejecutar la aplicación. Ejecutar la aplicación ejecuta el método DbInitializer.Initialize. DbInitializer.Initialize rellena la base de datos nueva.

Abra la base de datos en SSOX:

  • Si anteriormente se abrió SSOX, haga clic en el botón Actualizar.

  • Expanda el nodo Tablas. Se muestran las tablas creadas.

    Tablas en SSOX

  • Examine la tabla CourseAssignment:

    • Haga clic con el botón derecho en la tabla CourseAssignment y seleccione Ver datos.
    • Compruebe que la tabla CourseAssignment contiene datos.

    Datos de CourseAssignment en SSOX

Aplicar la migración

Esta sección es opcional. Estos pasos solo funcionan para SQL Server LocalDB y si ha pasado por alto la sección Quitar y volver a crear la base de datos anterior.

Cuando se ejecutan migraciones con datos existentes, puede haber restricciones de clave externa que no se cumplen con los datos existentes. Con los datos de producción, se deben realizar algunos pasos para migrar los datos existentes. En esta sección se proporciona un ejemplo de corrección de las infracciones de restricción de clave externa. No realice estos cambios de código sin hacer una copia de seguridad. No realice estos cambios de código si ha completado la sección anterior Quitar y volver a crear la base de datos.

El archivo {timestamp}_ComplexDataModel.cs contiene el código siguiente:

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

El código anterior agrega una clave externa DepartmentID que acepta valores NULL a la tabla Course. La base de datos del tutorial anterior contiene filas en Course, por lo que esa tabla no se puede actualizar mediante migraciones.

Para realizar la migración de ComplexDataModel, trabaje con los datos existentes:

  • Cambie el código para asignar a la nueva columna (DepartmentID) un valor predeterminado.
  • Cree un departamento falso denominado "Temp" para que actúe como el departamento predeterminado.

Corregir las restricciones de clave externa

En la clase de migración ComplexDataModel, actualice el método Up:

  • Abra el archivo {timestamp}_ComplexDataModel.cs.
  • Convierta en comentario la línea de código que agrega la columna DepartmentID a la tabla 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);

Agregue el código resaltado siguiente. El nuevo código va después del bloque .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);

Con los cambios anteriores, las filas Course existentes estarán relacionadas con el departamento "Temp" después de ejecutar el método ComplexDataModel.Up.

Para este tutorial se ha simplificado la manera de controlar la situación que se muestra aquí. Una aplicación de producción debería:

  • Incluir código o scripts para agregar filas de Department y filas de Course relacionadas a las nuevas filas de Department.
  • No use el departamento "Temp" o el valor predeterminado de Course.DepartmentID.
  • En la Consola del Administrador de paquetes (PMC), ejecute el comando siguiente:

    Update-Database
    

Como el método DbInitializer.Initialize está diseñado para funcionar solo con una base de datos vacía, use SSOX para eliminar todas las filas de las tablas Student y Course. (La eliminación en cascada se encargará de la tabla Enrollment).

Ejecutar la aplicación. Ejecutar la aplicación ejecuta el método DbInitializer.Initialize. DbInitializer.Initialize rellena la base de datos nueva.

Pasos siguientes

En los dos tutoriales siguientes se muestra cómo leer y actualizar los datos relacionados.

En los tutoriales anteriores se trabajaba con un modelo de datos básico que se componía de tres entidades. En este tutorial:

  • Se agregan más entidades y relaciones.
  • Se personaliza el modelo de datos especificando el formato, la validación y las reglas de asignación de la base de datos.

Las clases de entidad para el modelo de datos completado se muestran en la ilustración siguiente:

Diagrama de entidades

Si experimenta problemas que no puede resolver, descargue la aplicación completada.

Personalizar el modelo de datos con atributos

En esta sección, se personaliza el modelo de datos mediante atributos.

El atributo DataType

Las páginas de alumno actualmente muestran la hora de la fecha de inscripción. Normalmente, los campos de fecha muestran solo la fecha y no la hora.

Actualice Models/Student.cs con el siguiente código resaltado:

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

El atributo DataType especifica un tipo de datos más específico que el tipo intrínseco de base de datos. En este caso solo se debe mostrar la fecha, no la fecha y hora. La enumeración DataType proporciona muchos tipos de datos, como Date (Fecha), Time (Hora), PhoneNumber (Número de teléfono), Currency (Divisa), EmailAddress (Dirección de correo electrónico), etc. El atributo DataType también puede permitir que la aplicación proporcione automáticamente características específicas del tipo. Por ejemplo:

  • El vínculo mailto: se crea automáticamente para DataType.EmailAddress.
  • El selector de fecha se proporciona para DataType.Date en la mayoría de los exploradores.

El atributo DataType emite atributos HTML 5 data- (se pronuncia "datos dash") para su uso por parte de los exploradores HTML 5. Los atributos DataType no proporcionan validación.

DataType.Date no especifica el formato de la fecha que se muestra. De manera predeterminada, el campo de fecha se muestra según los formatos predeterminados basados en el elemento CultureInfo del servidor.

El atributo DisplayFormat se usa para especificar el formato de fecha de forma explícita:

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

La configuración ApplyFormatInEditMode especifica que el formato también debe aplicarse a la interfaz de usuario de edición. Algunos campos no deben usar ApplyFormatInEditMode. Por ejemplo, el símbolo de divisa generalmente no debe mostrarse en un cuadro de texto de edición.

El atributo DisplayFormat puede usarse por sí solo. Normalmente se recomienda usar el atributo DataType con el atributo DisplayFormat. El atributo DataType transmite la semántica de los datos en lugar de cómo se representan en una pantalla. El atributo DataType proporciona las siguientes ventajas que no están disponibles en DisplayFormat:

  • El explorador puede habilitar características de HTML5. Por ejemplo, mostrar un control de calendario, el símbolo de divisa adecuado según la configuración regional, vínculos de correo electrónico, validación de entradas del lado cliente, etc.
  • De manera predeterminada, el explorador representa los datos con el formato correcto según la configuración regional.

Para obtener más información, vea la documentación del asistente de etiquetas <entrada>.

Ejecutar la aplicación. Vaya a la página de índice de Students. Ya no se muestran las horas. Todas las vistas que usa el modelo Student muestran la fecha sin hora.

Página de índice de estudiantes en la que se muestran las fechas sin horas

El atributo StringLength

Las reglas de validación de datos y los mensajes de error de validación se pueden especificar con atributos. El atributo StringLength especifica la longitud mínima y máxima de caracteres que se permite en un campo de datos. El atributo StringLength también proporciona validación del lado cliente y del lado servidor. El valor mínimo no influye en el esquema de base de datos.

Actualice el modelo Student con el código siguiente:

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

El código anterior limita los nombres a no más de 50 caracteres. El atributo StringLength no impide que un usuario escriba un espacio en blanco para un nombre. El atributo RegularExpression se usa para aplicar restricciones a la entrada. Por ejemplo, el código siguiente requiere que el primer carácter sea una letra mayúscula y el resto de caracteres sean alfabéticos:

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

Ejecute la aplicación:

  • Vaya a la página Students.
  • Seleccione Create New y escriba un nombre más de 50 caracteres.
  • Seleccione Create, la validación del lado cliente muestra un mensaje de error.

Página de índice de estudiantes en la que se muestran errores de longitud de cadena

En el Explorador de objetos de SQL Server, (SSOX) abra el diseñador de tablas de Student haciendo doble clic en la tabla Student.

Tabla de estudiantes en SSOX antes de las migraciones

La imagen anterior muestra el esquema para la tabla Student. Los campos de nombre tienen tipo nvarchar(MAX) porque las migraciones no se han ejecutado en la base de datos. Cuando se ejecutan las migraciones más adelante en este tutorial, los campos de nombre se convierten en nvarchar(50).

El atributo Column

Los atributos pueden controlar cómo se asignan las clases y propiedades a la base de datos. En esta sección, el atributo Column se usa para asignar el nombre de la propiedad FirstMidName a "FirstName" en la base de datos.

Cuando se crea la base de datos, los nombres de propiedad en el modelo se usan para los nombres de columna (excepto cuando se usa el atributo Column).

El modelo Student usa FirstMidName para el nombre de campo por la posibilidad de que el campo contenga también un segundo nombre.

Actualice el archivo Student.cs con el siguiente código resaltado:

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

Con el cambio anterior, Student.FirstMidName en la aplicación se asigna a la columna FirstName de la tabla Student.

La adición del atributo Column cambia el modelo de respaldo de SchoolContext. El modelo que está haciendo la copia de seguridad de SchoolContext ya no coincide con la base de datos. Si la aplicación se ejecuta antes de aplicar las migraciones, se genera la siguiente excepción:

SqlException: Invalid column name 'FirstName'.

Para actualizar la base de datos:

  • Compile el proyecto.
  • Abra una ventana de comandos en la carpeta del proyecto. Escriba los comandos siguientes para crear una migración y actualizar la base de datos:
Add-Migration ColumnFirstName
Update-Database

El comando migrations add ColumnFirstName genera el siguiente mensaje de advertencia:

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

La advertencia se genera porque los campos de nombre ahora están limitados a 50 caracteres. Si un nombre en la base de datos tenía más de 50 caracteres, se perderían desde el 51 hasta el último carácter.

  • Pruebe la aplicación.

Abra la tabla de estudiantes en SSOX:

Tabla de estudiantes en SSOX después de las migraciones

Antes de aplicar la migración, las columnas de nombre eran de tipo nvarchar(MAX). Las columnas de nombre ahora son nvarchar(50). El nombre de columna ha cambiado de FirstMidName a FirstName.

Nota

En la sección siguiente, la creación de la aplicación en algunas de las fases genera errores del compilador. Las instrucciones especifican cuándo se debe compilar la aplicación.

Actualizar la entidad Student

Entidad Student

Actualice Models/Student.cs con el siguiente 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; }
    }
}

El atributo Required

El atributo Required hace que las propiedades de nombre sean campos obligatorios. El atributo Required no es necesario para los tipos que no aceptan valores NULL, como los tipos de valor (DateTime, int, double, etc.). Los tipos que no aceptan valores NULL se tratan automáticamente como campos obligatorios.

El atributo Required se podría reemplazar con un parámetro de longitud mínima en el atributo StringLength:

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

El atributo Display

El atributo Display especifica que el título de los cuadros de texto debe ser "First Name" (Nombre), "Last Name" (Apellidos), "Full Name" (Nombre completo) y "Enrollment Date" (Fecha de inscripción). El título predeterminado no tiene espacios dividiendo las palabras, por ejemplo, "Nombrecompleto".

La propiedad calculada FullName

FullName es una propiedad calculada que devuelve un valor que se crea mediante la concatenación de otras dos propiedades. No se puede establecer FullName, tiene solo un descriptor de acceso get. No se crea ninguna columna FullName en la base de datos.

Crear la entidad Instructor

La entidad Instructor

Cree el archivo Models/Instructor.cs con el siguiente 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; }
    }
}

En una sola línea puede haber varios atributos. Los atributos HireDate pudieron escribirse de la manera siguiente:

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

Las propiedades de navegación CourseAssignments y OfficeAssignment

CourseAssignments y OfficeAssignment son propiedades de navegación.

Un instructor puede impartir cualquier número de cursos, por lo que CourseAssignments se define como una colección.

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

Si una propiedad de navegación contiene varias entidades:

  • Debe ser un tipo de lista, donde se pueden agregar, eliminar y actualizar las entradas.

Los tipos de propiedad de navegación incluyen:

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

Si se especifica ICollection<T>, EF Core crea una colección HashSet<T> de forma predeterminada.

La entidad CourseAssignment se explica en la sección sobre las relaciones de varios a varios.

Las reglas de negocio de Contoso University establecen que un instructor puede tener, a lo sumo, una oficina. La propiedad OfficeAssignment contiene una única instancia de OfficeAssignment. OfficeAssignment es NULL si no se asigna ninguna oficina.

public OfficeAssignment OfficeAssignment { get; set; }

Crear la entidad OfficeAssignment

Entidad OfficeAssignment

Cree el archivo Models/OfficeAssignment.cs con el siguiente 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; }
    }
}

El atributo Key

El atributo [Key] se usa para identificar una propiedad como la clave principal (PK) cuando el nombre de propiedad es diferente de classnameID o ID.

Hay una relación de uno a cero o uno entre las entidades Instructor y OfficeAssignment. Solo existe una asignación de oficina en relación con el instructor a la que está asignada. La clave principal de OfficeAssignment también es la clave externa (FK) para la entidad Instructor. EF Core no reconoce automáticamente InstructorID como la clave principal de OfficeAssignment porque:

  • InstructorID no sigue la convención de nomenclatura de ID o classnameID.

Por tanto, se usa el atributo Key para identificar InstructorID como la clave principal:

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

De forma predeterminada, EF Core trata la clave como no generada por la base de datos porque la columna es para una relación de identificación.

La propiedad de navegación Instructor

La propiedad de navegación OfficeAssignment para la entidad Instructor acepta valores NULL porque:

  • Los tipos de referencia, como las clases, aceptan valores NULL.
  • Un instructor podría no tener una asignación de oficina.

La entidad OfficeAssignment tiene una propiedad de navegación Instructor que no acepta valores NULL porque:

  • InstructorID no acepta valores NULL.
  • Una asignación de oficina no puede existir sin un instructor.

Cuando una entidad Instructor tiene una entidad OfficeAssignment relacionada, cada entidad tiene una referencia a la otra en su propiedad de navegación.

El atributo [Required] puede aplicarse a la propiedad de navegación Instructor:

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

El código anterior especifica que debe haber un instructor relacionado. El código anterior no es necesario porque la clave externa InstructorID, que también es la clave principal, no acepta valores NULL.

Modificar la entidad Course

La entidad Course

Actualice Models/Course.cs con el siguiente 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; }
    }
}

La entidad Course tiene una propiedad de clave externa (FK) DepartmentID. DepartmentID apunta a la entidad relacionada Department. La entidad Course tiene una propiedad de navegación Department.

EF Core no requiere una propiedad de clave externa para un modelo de datos cuando el modelo tiene una propiedad de navegación para una entidad relacionada.

EF Core crea automáticamente claves externas en la base de datos siempre que se necesiten. EF Core crea propiedades paralelas para las claves externas creadas automáticamente. Tener la clave externa en el modelo de datos puede hacer que las actualizaciones sean más sencillas y eficaces. Por ejemplo, considere la posibilidad de un modelo donde la propiedad de la clave externa DepartmentIDno está incluida. Cuando se captura una entidad de curso para editar:

  • La entidad Department es NULL si no se carga explícitamente.
  • Para actualizar la entidad Course, la entidad Department debe capturarse en primer lugar.

Cuando se incluye la propiedad de clave externa DepartmentID en el modelo de datos, no es necesario capturar la entidad Department antes de una actualización.

El atributo DatabaseGenerated

El atributo [DatabaseGenerated(DatabaseGeneratedOption.None)] especifica que la aplicación proporciona la clave principal, en lugar de generarla la base de datos.

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

De forma predeterminada, EF Core da por supuesto que la base de datos genera valores de clave principal. Los valores de clave principal generados por la base de datos suelen ser el mejor método. Para las entidades Course, el usuario especifica la clave principal. Por ejemplo, un número de curso como una serie de 1000 para el departamento de matemáticas, una serie de 2000 para el departamento de inglés.

También se puede usar el atributo DatabaseGenerated para generar valores predeterminados. Por ejemplo, la base de datos puede generar automáticamente un campo de fecha para registrar la fecha en que se crea o actualiza una fila. Para obtener más información, vea Propiedades generadas.

Propiedades de clave externa y de navegación

Las propiedades de clave externa (FK) y las de navegación de la entidad Course reflejan las relaciones siguientes:

Un curso se asigna a un departamento, por lo que hay una clave externa DepartmentID y una propiedad de navegación Department.

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

Un curso puede tener cualquier número de alumnos inscritos en él, por lo que la propiedad de navegación Enrollments es una colección:

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

Un curso puede ser impartido por varios instructores, por lo que la propiedad de navegación CourseAssignments es una colección:

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

CourseAssignment se explica más adelante.

Crear la entidad Department

La entidad Department

Cree el archivo Models/Department.cs con el siguiente 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; }
    }
}

El atributo Column

Anteriormente se usó el atributo Column para cambiar la asignación de nombres de columna. En el código de la entidad Department, se usó el atributo Column para cambiar la asignación de tipos de datos de SQL. La columna Budget se define mediante el tipo money de SQL Server en la base de datos:

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

Por lo general, la asignación de columnas no es necesaria. EF Core generalmente elige el tipo de datos de SQL Server apropiado en función del tipo CLR para la propiedad. El tipo CLR decimal se asigna a un tipo decimal de SQL Server. Budget es para la divisa, y el tipo de datos money es más adecuado para la divisa.

Propiedades de clave externa y de navegación

Las propiedades de clave externa y de navegación reflejan las relaciones siguientes:

  • Un departamento puede tener o no un administrador.
  • Un administrador siempre es un instructor. Por lo tanto, la propiedad InstructorID se incluye como la clave externa para la entidad Instructor.

La propiedad de navegación se denomina Administrator pero contiene una entidad Instructor:

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

El signo de interrogación (?) en el código anterior especifica que la propiedad acepta valores NULL.

Un departamento puede tener varios cursos, por lo que hay una propiedad de navegación Courses:

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

Nota: Por convención, EF Core permite la eliminación en cascada para las claves externas que no aceptan valores NULL y para las relaciones de varios a varios. La eliminación en cascada puede dar lugar a reglas de eliminación en cascada circular. Las reglas de eliminación en cascada circular provocan una excepción cuando se agrega una migración.

Por ejemplo, si la propiedad Department.InstructorID no se ha definido como que acepta valores NULL:

  • EF Core configura una regla de eliminación en cascada para eliminar el departamento cuando se elimina el instructor.

  • Eliminar el departamento cuando se elimine el instructor no es el comportamiento previsto.

  • La API fluida siguiente establecería una regla de restricción en lugar de en cascada.

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

El código anterior deshabilita la eliminación en cascada en la relación de instructor y departamento.

Actualizar la entidad Enrollment

Un registro de inscripción corresponde a un curso realizado por un alumno.

La entidad Enrollment

Actualice Models/Enrollment.cs con el siguiente 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; }
    }
}

Propiedades de clave externa y de navegación

Las propiedades de clave externa y de navegación reflejan las relaciones siguientes:

Un registro de inscripción es para un curso, por lo que hay una propiedad de clave externa CourseID y una propiedad de navegación Course:

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

Un registro de inscripción es para un alumno, por lo que hay una propiedad de clave externa StudentID y una propiedad de navegación Student:

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

Relaciones Varios a Varios

Hay una relación de varios a varios entre las entidades Student y Course. La entidad Enrollment funciona como una tabla combinada varios a varios con carga útil en la base de datos. "Con carga útil" significa que la tabla Enrollment contiene datos adicionales, además de claves externas de las tablas combinadas (en este caso, la clave principal y Grade).

En la ilustración siguiente se muestra el aspecto de estas relaciones en un diagrama de entidades. (Este diagrama se generó con EF Power Tools para EF 6.x. La creación del diagrama no forma parte del tutorial).

Relación de varios a varios entre estudiantes y cursos

Cada línea de relación tiene un 1 en un extremo y un asterisco (*) en el otro, para indicar una relación uno a varios.

Si la tabla Enrollment no incluyera información de calificaciones, solo tendría que contener las dos claves externas (CourseID y StudentID). Una tabla combinada de varios a varios sin carga útil se suele denominar una tabla combinada pura (PJT).

Las entidades Instructor y Course tienen una relación de varios a varios con una tabla combinada pura.

Nota: EF 6.x es compatible con las tablas de combinación implícitas para relaciones de varios a varios, pero EF Core no. Para obtener más información, consulte Relaciones de varios a varios en EF Core 2.0.

La entidad CourseAssignment

La entidad CourseAssignment

Cree el archivo Models/CourseAssignment.cs con el siguiente 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; }
    }
}

Relación Instructor-to-Courses

Relación de varios a varios Instructor-to-Courses

La relación de varios a varios Instructor-to-Courses:

  • Requiere una tabla de combinación que debe estar representada por un conjunto de entidades.
  • Es una tabla combinada pura (tabla sin carga útil).

Es común denominar una entidad de combinación EntityName1EntityName2. Por ejemplo, la tabla de combinación Instructor-to-Courses usando este patrón es CourseInstructor. Pero se recomienda usar un nombre que describa la relación.

Los modelos de datos empiezan de manera sencilla y crecen. Las tablas combinadas sin carga útil (PJT) evolucionan con frecuencia para incluir la carga útil. A partir de un nombre de entidad descriptivo, no es necesario cambiar el nombre cuando la tabla combinada cambia. Idealmente, la entidad de combinación tendrá su propio nombre natural (posiblemente una sola palabra) en el dominio de empresa. Por ejemplo, Books y Customers podrían vincularse a través de una entidad combinada denominada Ratings. Para la relación de varios a varios Instructor-to-Courses, se prefiere CourseAssignment a CourseInstructor.

Clave compuesta

Las claves externas no aceptan valores NULL. Las dos claves externas en CourseAssignment (InstructorID y CourseID) juntas identifican de forma única cada fila de la tabla CourseAssignment. CourseAssignment no requiere una clave principal dedicada. Las propiedades InstructorID y CourseID funcionan como una clave principal compuesta. La única manera de especificar claves principales compuestas en EF Core es con la API fluida. La sección siguiente muestra cómo configurar la clave principal compuesta.

La clave compuesta asegura que:

  • Se permiten varias filas para un curso.
  • Se permiten varias filas para un instructor.
  • No se permiten varias filas para el mismo instructor y curso.

La entidad de combinación Enrollment define su propia clave principal, por lo que este tipo de duplicados son posibles. Para evitar los duplicados:

  • Agregue un índice único en los campos de clave externa, o
  • Configure Enrollment con una clave compuesta principal similar a CourseAssignment. Para obtener más información, vea Índices.

Actualizar el contexto de la base de datos

Agregue el código resaltado siguiente 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 });
        }
    }
}

El código anterior agrega las nuevas entidades y configura la clave principal compuesta de la entidad CourseAssignment.

Alternativa de la API fluida a los atributos

El método OnModelCreating del código anterior usa la API fluida para configurar el comportamiento de EF Core. La API se denomina "fluida" porque a menudo se usa para encadenar una serie de llamadas de método en una única instrucción. El código siguiente es un ejemplo de la API fluida:

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

En este tutorial, la API fluida se usa solo para la asignación de base de datos que no se puede realizar con atributos. Pero la API fluida puede especificar casi todas las reglas de formato, validación y asignación que se pueden realizar mediante el uso de atributos.

Algunos atributos como MinimumLength no se pueden aplicar con la API fluida. MinimumLength no cambia el esquema, solo aplica una regla de validación de longitud mínima.

Algunos desarrolladores prefieren usar la API fluida exclusivamente para mantener "limpias" sus clases de entidad. Se pueden combinar los atributos y la API fluida. Hay algunas configuraciones que solo se pueden realizar con la API fluida (especificando una clave principal compuesta). Hay algunas configuraciones que solo se pueden realizar con atributos (MinimumLength). La práctica recomendada para el uso de atributos o API fluida:

  • Elija uno de estos dos enfoques.
  • Use el enfoque elegido de forma tan coherente como sea posible.

Algunos de los atributos utilizados en este tutorial se usan para:

  • Solo validación (por ejemplo, MinimumLength).
  • Solo configuración de EF Core (por ejemplo, HasKey).
  • Validación y configuración de EF Core (por ejemplo, [StringLength(50)]).

Para obtener más información sobre la diferencia entre los atributos y la API fluida, vea Métodos de configuración.

Diagrama de entidades en el que se muestran las relaciones

En la siguiente ilustración se muestra el diagrama creado por EF Power Tools para el modelo School completado.

Diagrama de entidades

El diagrama anterior muestra:

  • Varias líneas de relación uno a varios (1 a *).
  • La línea de relación de uno a cero o uno (1 a 0..1) entre las entidades Instructor y OfficeAssignment.
  • La línea de relación de cero o uno o varios (0..1 a *) entre las entidades Instructor y Department.

Inicializar la base de datos con datos de prueba

Actualice el código de 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();
        }
    }
}

El código anterior proporciona datos de inicialización para las nuevas entidades. La mayor parte de este código crea objetos de entidad y carga los datos de ejemplo. Los datos de ejemplo se usan para pruebas. Consulte Enrollments y CourseAssignments para obtener ejemplos de cómo pueden inicializarse las tablas de combinación de varios a varios.

Agregar una migración

Compile el proyecto.

Add-Migration ComplexDataModel

El comando anterior muestra una advertencia sobre la posible pérdida de datos.

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'

Si se ejecuta el comando database update, se genera el error siguiente:

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 la migración

Ahora que tiene una base de datos existente, debe pensar cómo aplicar los cambios futuros en ella. En este tutorial se muestran dos enfoques:

Quitar y volver a crear la base de datos

El código en la DbInitializer actualizada agrega los datos de inicialización para las nuevas entidades. Para obligar a EF Core a crear una base de datos, quite y actualice la base de datos:

En la Consola del Administrador de paquetes (PMC), ejecute el comando siguiente:

Drop-Database
Update-Database

Ejecute Get-Help about_EntityFrameworkCore desde PMC para obtener información de ayuda.

Ejecutar la aplicación. Ejecutar la aplicación ejecuta el método DbInitializer.Initialize. DbInitializer.Initialize rellena la base de datos nueva.

Abra la base de datos en SSOX:

  • Si anteriormente se abrió SSOX, haga clic en el botón Actualizar.
  • Expanda el nodo Tablas. Se muestran las tablas creadas.

Tablas en SSOX

Examine la tabla CourseAssignment:

  • Haga clic con el botón derecho en la tabla CourseAssignment y seleccione Ver datos.
  • Compruebe que la tabla CourseAssignment contiene datos.

Datos de CourseAssignment en SSOX

Aplicar la migración a la base de datos existente

Esta sección es opcional. Estos pasos solo funcionan si pasó por alto la sección Quitar y volver a crear la base de datos.

Cuando se ejecutan migraciones con datos existentes, puede haber restricciones de clave externa que no se cumplen con los datos existentes. Con los datos de producción, se deben realizar algunos pasos para migrar los datos existentes. En esta sección se proporciona un ejemplo de corrección de las infracciones de restricción de clave externa. No realice estos cambios de código sin hacer una copia de seguridad. No realice estos cambios de código si realizó la sección anterior y actualizó la base de datos.

El archivo {timestamp}_ComplexDataModel.cs contiene el código siguiente:

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

El código anterior agrega una clave externa DepartmentID que acepta valores NULL a la tabla Course. La base de datos del tutorial anterior contiene filas en Course, por lo que no se puede actualizar esa tabla mediante migraciones.

Para realizar la migración de ComplexDataModel, trabaje con los datos existentes:

  • Cambie el código para asignar a la nueva columna (DepartmentID) un valor predeterminado.
  • Cree un departamento falso denominado "Temp" para que actúe como el departamento predeterminado.

Corregir las restricciones de clave externa

Actualice el método Up de las clases ComplexDataModel:

  • Abra el archivo {timestamp}_ComplexDataModel.cs.
  • Convierta en comentario la línea de código que agrega la columna DepartmentID a la tabla 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);

Agregue el código resaltado siguiente. El nuevo código va después del bloque .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);

Con los cambios anteriores, las filas Course existentes estarán relacionadas con el departamento "Temp" después de ejecutar el método ComplexDataModel Up.

Una aplicación de producción debería:

  • Incluir código o scripts para agregar filas de Department y filas de Course relacionadas a las nuevas filas de Department.
  • No use el departamento "Temp" o el valor predeterminado de Course.DepartmentID.

El siguiente tutorial trata los datos relacionados.

Recursos adicionales