Tutorial: crear un modelo de datos más complejo para una aplicación MVC de ASP.NET

En los tutoriales anteriores ha trabajado con un modelo de datos simple que se componía de tres entidades. En este tutorial agregará más entidades y relaciones, y personalizará el modelo de datos mediante la especificación de reglas de formato, validación y asignación de base de datos. Verá dos maneras de personalizar el modelo de datos: mediante la adición de atributos a clases de entidad y mediante la adición de código a la clase de contexto de base de datos.

Cuando haya terminado, las clases de entidad conformarán el modelo de datos completo que se muestra en la ilustración siguiente:

School_class_diagram

En este tutorial ha:

  • Personalizar el modelo de datos
  • Actualizar la entidad Student
  • Crea la entidad Instructor
  • Crea la entidad OfficeAssignment
  • Modificar la entidad Course
  • Crear la entidad Department
  • Modificar la entidad Enrollment
  • Agregar código al contexto de la base de datos
  • Inicializa la base de datos con datos de prueba
  • Agregar una migración
  • Actualizar la base de datos

Requisitos previos

Personalizar el modelo de datos

En esta sección verá cómo personalizar el modelo de datos mediante el uso de atributos que especifican reglas de formato, validación y asignación de base de datos. Después, en varias de las secciones siguientes, creará el modelo de datos School completo agregando atributos a las clases que ya ha creado y creando clases para los demás tipos de entidad del modelo.

El atributo DataType

Para las fechas de inscripción de estudiantes, en todas las páginas web se muestra actualmente la hora junto con la fecha, aunque todo lo que le interesa para este campo es 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 cada vista en la que se muestren los datos. Para ver un ejemplo de cómo hacerlo, deberá agregar un atributo a la propiedad EnrollmentDate en la clase Student.

En Models/Student.cs, agregue una instrucción using para el espacio de nombres System.ComponentModel.DataAnnotations y los atributos DataType y DisplayFormat a la propiedad EnrollmentDate, como se muestra en el ejemplo siguiente:

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 virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

El atributo DataType se usa para especificar un tipo de datos más específico que el tipo intrínseco de base de datos. En este caso solo se quiere realizar el seguimiento de la fecha, no de la fecha y la hora. La enumeración DataType proporciona muchos tipos de datos, comoDate, Time, PhoneNumber, Currency, EmailAddress, etc. El atributo DataType también puede permitir que la aplicación proporcione automáticamente características específicas del tipo. Por ejemplo, se puede crear un vínculo mailto: para DataType.EmailAddress y se puede proporcionar un selector de fechas para DataType.Date en exploradores compatibles con HTML5. Los atributos DataType emiten atributos data- de HTML 5 que los exploradores HTML 5 pueden comprender. Los atributos DataType no proporcionan ninguna validación.

DataType.Date no especifica el formato de la fecha que se muestra. De manera predeterminada, el campo de datos 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)]

El valor ApplyFormatInEditMode especifica que el formato indicado se debe aplicar también cuando el valor se muestra en un cuadro de texto para su edición. (Es posible que no le interese ese comportamiento para algunos campos, por ejemplo, para los valores de moneda, es posible que no quiera que el símbolo de la moneda se incluya en el cuadro de texto para modificarlo).

Puede usar el atributo DisplayFormat por sí solo, pero normalmente se recomienda usar también el atributo DataType. El atributo DataType transmite la semántica de los datos en contraposición a cómo se representan en una pantalla y ofrece las siguientes ventajas que no se obtienen con DisplayFormat:

  • El explorador puede habilitar características de HTML5 (por ejemplo, para 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.
  • El atributo DataType puede habilitar MVC para elegir la plantilla de campo adecuada para representar los datos (DisplayFormat utiliza la plantilla de cadena). Para más información, consulte Plantillas de MVC 2 de ASP.NET de Brad Wilson. (Aunque está escrito para MVC 2, este artículo todavía se aplica a la versión actual de ASP.NET MVC).

Si usa el atributo DataType con un campo de fecha, también debe especificar el atributo DisplayFormat para asegurarse de que el campo se representa correctamente en los exploradores Chrome. Para más información, consulte esta conversación de StackOverflow.

Para obtener más información sobre cómo controlar otros formatos de fecha en MVC, vaya a Introducción a MVC 5: examinar los métodos de edición y editar vista y busque en la página "internacionalización".

Ejecute la página Students Index y verá que ya no se muestran las horas para las fechas de inscripción. Lo mismo sucede para cualquier vista en la que se use el modelo Student.

Students_index_page_with_formatted_date

StringLengthAttribute

También puede especificar reglas de validación de datos y mensajes de error de validación mediante atributos. El atributo StringLength establece la longitud máxima de la base de datos y proporciona la validación del lado cliente y el lado servidor para MVC de ASP.NET. En este atributo también se puede especificar la longitud mínima de la cadena, pero el valor mínimo no influye en el esquema de la base de datos.

Imagine que quiere asegurarse de que los usuarios no escriban más de 50 caracteres para un nombre. Para agregar esta limitación, agregue atributos StringLength a las propiedades LastName y FirstMidName, como se muestra en el ejemplo 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 virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

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

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

El atributo MaxLength proporciona una funcionalidad similar a la del atributo StringLength pero no proporciona validación del lado cliente.

Ejecute la aplicación y haga clic en la pestaña Students. Se muestra el siguiente error:

El modelo que respalda al contexto "SchoolContext" ha cambiado desde que se creó la base de datos. Considere la posibilidad de usar Migraciones de Code First para actualizar la base de datos (https://go.microsoft.com/fwlink/?LinkId=238269).

Ahora el modelo de base de datos ha cambiado de tal forma que se necesita un cambio en el esquema de la base de datos y Entity Framework lo ha detectado. Usará migraciones para actualizar el esquema sin perder los datos que ha agregado a la base de datos mediante la interfaz de usuario. Si ha cambiado los datos creados por el método Seed, se volverán a cambiar a su estado original debido al método AddOrUpdate que usa en el método Seed. (AddOrUpdate es equivalente a una operación "upsert" de la terminología de la base de datos).

En la Consola del Administrador de paquetes (PMC), escriba los comandos siguientes:

add-migration MaxLengthOnNames
update-database

El comando add-migration crea un archivo denominado <timeStamp>_MaxLengthOnNames.cs. Este archivo contiene código en el método Up que actualizará la base de datos para que coincida con el modelo de datos actual. El comando update-database ejecutó ese código.

Entity Framework usa la marca de tiempo que precede al nombre de archivo de migraciones para ordenar las migraciones. Puede crear varias migraciones antes de ejecutar el comando update-database y, después, todas las migraciones se aplican en el orden en el que se hayan creado.

Ejecute la página Create y escriba un nombre de más de 50 caracteres. Al hacer clic en Crear, la validación del lado cliente muestra un mensaje de error: El campo LastName debe ser una cadena con una longitud máxima de 50.

El atributo Column

También puede usar atributos para controlar cómo se asignan las clases y propiedades a la base de datos. Imagine que hubiera usado el nombre FirstMidName para el nombre de campo por la posibilidad de que el campo contenga también un segundo nombre. Pero quiere que la columna de base de datos se denomine FirstName, ya que los usuarios que van a escribir consultas ad hoc en la base de datos están acostumbrados a ese nombre. Para realizar esta asignación, puede usar el atributo Column.

El atributo Column especifica que, cuando se cree la base de datos, la columna de la tabla Student que se asigna a la propiedad FirstMidName se denominará FirstName. En otras palabras, cuando el código hace referencia a Student.FirstMidName, los datos procederán o se actualizarán en la columna FirstName de la tabla Student. Si no especifica nombres de columna, se les asigna el mismo nombre que el de la propiedad.

En el archivo Student.cs, agregue una instrucción using para System.ComponentModel.DataAnnotations.Schema y agregue el atributo de nombre de columna a la propiedad FirstMidName, como se muestra en el código resaltado siguiente:

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 virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

La adición del atributo Column cambia el modelo de respaldo de SchoolContext, por lo que no coincidirá con la base de datos. Escriba los siguientes comandos en la consola de administración de paquetes (PMC) para crear otra migración:

add-migration ColumnFirstName
update-database

En el Explorador de servidores, abra el diseñador de tablas Student haciendo doble clic en la tabla Student.

En la imagen siguiente se muestra el nombre de columna original tal y como estaba antes de aplicar las dos primeras migraciones. Además del nombre de columna que cambia de FirstMidName a FirstName, las dos columnas de nombre han cambiado de una longitud MAX a 50 caracteres.

Two screenshots that show the differences in the Name and Data Type of the two Student tables.

También puede realizar cambios en la asignación de bases de datos mediante la API fluida, como verá más adelante en este tutorial.

Nota:

Si intenta compilar antes de terminar de crear todas las clases de entidad en las secciones siguientes, es posible que se produzcan errores del compilador.

Actualizar la entidad Student

En Models/Student.cs, reemplace el código que ha agregado antes con el código siguiente. Los cambios aparecen resaltados.

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 virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

El atributo Required

El atributo Required hace que las propiedades de nombre sean campos obligatorios. El Required attribute no es necesario para tipos de valor como DateTime, int, double y float. A los tipos de valor no se les puede asignar un valor null, por lo que son tratados de forma inherente 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 un control total sobre la cadena.

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) en lugar del nombre de propiedad de cada instancia (que no tiene ningún espacio para dividir las palabras).

La propiedad calculada FullName

FullName es una propiedad calculada que devuelve un valor que se crea mediante la concatenación de otras dos propiedades. Por tanto, solo tiene un descriptor de acceso get y no se generará ninguna columna FullName en la base de datos.

Crea la entidad Instructor

Cree Models/Instructor.cs y reemplace el código de plantilla con el código siguiente:

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 virtual ICollection<Course> Courses { get; set; }
        public virtual OfficeAssignment OfficeAssignment { get; set; }
    }
}

Tenga en cuenta que varias propiedades son las mismas en las entidades Student y Instructor. En el tutorial Implementación de la herencia más adelante en esta serie, deberá refactorizar este código para eliminar la redundancia.

Puede colocar varios atributos en una línea, por lo que también podría escribir la clase Instructor como se indica a continuación:

public class Instructor
{
   public int ID { get; set; }

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

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

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

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

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

Las propiedades de navegación Courses y OfficeAssignment

Courses y OfficeAssignment son propiedades de navegación. Como se ha explicado antes, normalmente se definen como virtuales para que puedan aprovechar las ventajas de una característica de Entity Framework denominada carga diferida. Además, si una propiedad de navegación puede contener varias entidades, su tipo debe implementar la interfaz ICollection<T>. Por ejemplo, IList<T> califica pero no IEnumerable<T>, porque IEnumerable<T> no implementa Agregar.

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

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

Nuestras reglas de negocio establecen que un instructor solo puede tener como máximo una oficina, por lo que OfficeAssignment se define como una sola entidad OfficeAssignment (que puede ser null si no se asigna ninguna oficina).

public virtual OfficeAssignment OfficeAssignment { get; set; }

Crea la entidad OfficeAssignment

Cree Models/OfficeAssignment.cs con el código siguiente:

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

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

        public virtual Instructor Instructor { get; set; }
    }
}

Compile el proyecto, que guarda los cambios y comprueba que no ha cometido ningún error de copia y pegado que el compilador puede detectar.

Atributo Key

Hay una relación de uno a cero o uno entre Instructor y las entidades OfficeAssignment. Una asignación de oficina solo existe en relación con el instructor al que se asigna y, por tanto, su clave principal también es su clave externa para la entidad Instructor. Pero Entity Framework no reconoce automáticamente InstructorID como la clave principal de esta entidad porque su nombre no sigue la convención de nomenclatura de ID o nombre_de_claseID. Por tanto, se usa el atributo Key para identificarla como la clave:

[Key]
[ForeignKey("Instructor")]
public int InstructorID { get; set; }

También puede usar el atributo Key si la entidad tiene su propia clave principal, pero querrá asignar un nombre a la propiedad que no sea classnameID o ID. De manera predeterminada, en EF la clave se trata como no generada por la base de datos porque la columna es para una relación de identificación.

Atributo ForeignKey

Cuando hay una relación de uno a cero o de uno a uno entre dos entidades (por ejemplo, entre OfficeAssignment y Instructor), EF no puede resolver qué extremo de la relación es la entidad de seguridad y cuál es el extremo dependiente. Las relaciones uno a uno tienen una propiedad de navegación de referencia en cada clase a la otra clase. El atributo ForeignKey se puede aplicar a la clase dependiente para establecer la relación. Si omite el atributo ForeignKey, se producirá el siguiente error al intentar crear la migración:

No se puede determinar el extremo principal de una asociación entre los tipos "ContosoUniversity.Models.OfficeAssignment" y "ContosoUniversity.Models.Instructor". El extremo principal de esta asociación se debe configurarse explícitamente mediante la API fluida de la relación o con anotaciones de datos.

Más adelante en el tutorial se muestra cómo configurar esta relación con la API fluida.

La propiedad de navegación Instructor

La entidad Instructor tiene una propiedad de navegación OfficeAssignment que admite un valor NULL (porque es posible que no se asigne una oficina a un instructor), y la entidad OfficeAssignment tiene una propiedad de navegación Instructor que no admite un valor NULL (porque una asignación de oficina no puede existir sin un instructor; InstructorID no admite valores NULL). Cuando una entidad Instructor tiene una entidad OfficeAssignment relacionada, cada entidad tiene una referencia a la otra en su propiedad de navegación.

Podría incluir un atributo [Required] en la propiedad de navegación Instructor para especificar que debe haber un instructor relacionado, pero no es necesario hacerlo porque la clave externa InstructorID (que también es la clave para esta tabla) no acepta valores NULL.

Modificar la entidad Course

En Models/Course.cs, reemplace el código que ha agregado antes con el código siguiente:

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 virtual Department Department { get; set; }
      public virtual ICollection<Enrollment> Enrollments { get; set; }
      public virtual ICollection<Instructor> Instructors { get; set; }
   }
}

La entidad Course tiene una propiedad de clave externa DepartmentID que apunta a la entidad Department relacionada y tiene una propiedad de navegación Department. Entity Framework no requiere que agregue una propiedad de clave externa al modelo de datos cuando tenga 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. Pero tener la clave externa en el modelo de datos puede hacer que las actualizaciones sean más sencillas y eficaces. Por ejemplo, al recuperar una entidad course para modificarla, la entidad Department es NULL si no la carga, por lo que cuando se actualiza la entidad course, primero tendrá que capturar la entidad Department. Cuando la propiedad de clave externa DepartmentID se incluye en el modelo de datos, no es necesario capturar la entidad Department antes de la actualización.

Atributo DatabaseGenerated

El atributo DatabaseGenerated con el parámetro None en la propiedad CourseID especifica que los valores de clave principal los proporciona el usuario, en lugar de que los genere la base de datos.

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

De manera predeterminada, Entity Framework da por supuesto que la base de datos genera los valores de clave principal. Es lo que le interesa en la mayoría de los escenarios. Pero para las entidades Course, usará un número de curso especificado por el usuario, por ejemplo, una serie 1000 para un departamento, una serie 2000 para otro y así sucesivamente.

Propiedades de clave externa y de navegación

Las propiedades de clave externa 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 por las razones mencionadas anteriormente.

    public int DepartmentID { get; set; }
    public virtual 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 virtual 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 virtual ICollection<Instructor> Instructors { get; set; }
    

Crear la entidad Department

Cree Models/Department.cs con el código siguiente:

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 virtual Instructor Administrator { get; set; }
      public virtual ICollection<Course> Courses { get; set; }
   }
}

El atributo Column

Anteriormente ha usado el atributo Column para cambiar la asignación de nombres de columna. En el código de la entidad Department, se usa el atributo Column para cambiar la asignación de tipos de datos de SQL para que la columna se defina con 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, ya que Entity Framework elige el tipo de datos de SQL Server adecuado en función del tipo CLR que defina para la propiedad. El tipo CLR decimal se asigna a un tipo decimal de SQL Server. Pero en este caso sabe que la columna va a contener cantidades de moneda, y el tipo de datos money es más adecuado para eso. Para obtener más información sobre los tipos de datos CLR y cómo coinciden con los tipos de datos de SQL Server, consulte SqlClient para Entity FrameworkTypes.

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, y un administrador es siempre un instructor. Por tanto, la propiedad InstructorID se incluye como la clave externa de la entidad Instructor y se agrega un signo de interrogación después de la designación del tipo int para marcar la propiedad como que admite un valor NULL. El nombre de la propiedad de navegación es Administrator pero contiene una entidad Instructor:

    public int? InstructorID { get; set; }
    public virtual Instructor Administrator { get; set; }
    
  • Un departamento puede tener varios cursos, por lo que hay una propiedad de navegación Courses:

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

    Nota:

    Por convención, Entity Framework permite la eliminación en cascada para las claves externas que no aceptan valores NULL y para las relaciones de varios a varios. Esto puede dar lugar a reglas de eliminación en cascada circulares, lo que producirá una excepción al intentar agregar una migración. Por ejemplo, si no ha definido la propiedad Department.InstructorID como que admite un valor NULL, obtendrá el siguiente mensaje de excepción: "La relación referencial dará lugar a una referencia cíclica que no está permitida". Si las reglas de negocio requerían que la propiedad InstructorID no acepte valores NULL, tendría que usar la siguiente instrucción de la API fluida para deshabilitar la eliminación en cascada en la relación:

modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);

Modificar la entidad Enrollment

En Models/Enrollment.cs, reemplace el código que ha agregado antes con el código siguiente.

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 virtual Course Course { get; set; }
        public virtual 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 solo curso, por lo que hay una propiedad de clave externa CourseID y una propiedad de navegación Course:

    public int CourseID { get; set; }
    public virtual Course Course { get; set; }
    
  • Un registro de inscripción es para un solo estudiante, por lo que hay una propiedad de clave externa StudentID y una propiedad de navegación Student:

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

Relaciones Varios a Varios

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

En la ilustración siguiente se muestra el aspecto de estas relaciones en un diagrama de entidades. (Este diagrama se ha generado mediante Entity Framework Power Tools; la creación del diagrama no forma parte del tutorial, simplemente se usa aquí como una ilustración).

Student-Course_many-to-many_relationship

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. En ese caso, se correspondería a una tabla de combinación de varios a varios sin carga (o una tabla de combinación pura) en la base de datos, y no tendría que crear una clase de modelo para ella en absoluto. Las entidades Instructor y Course tienen ese tipo de relación de varios a varios y, como puede ver, no hay ninguna clase de entidad entre ellas:

Instructor-Course_many-to-many_relationship

Pero en la base de datos se necesita una tabla de combinación, como se muestra en el diagrama de base de datos siguiente:

Instructor-Course_many-to-many_relationship_tables

Entity Framework crea automáticamente la tabla CourseInstructor, y la lee y actualiza indirectamente al leer y actualizar las propiedades de navegación Instructor.Courses y Course.Instructors.

Diagrama de relación entre entidades

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

School_data_model_diagram

Además de las líneas de relaciones de varios a varios (* a *) y las de relaciones de uno a varios (1 a *), aquí puede ver la línea de relación de uno a cero o uno (1 a 0..1) entre las entidades Instructor y OfficeAssignment, y la línea de relación de cero o uno a varios (0..1 a *) entre las entidades Instructor y Department.

Agregar código al contexto de la base de datos

A continuación agregará las nuevas entidades a la clase SchoolContext y personalizará parte de la asignación mediante llamadas a la API fluida. La API se denomina "fluida" porque a menudo se usa encadenando una serie de llamadas de método entre sí en una única instrucción, como en el siguiente ejemplo:

modelBuilder.Entity<Course>()
     .HasMany(c => c.Instructors).WithMany(i => i.Courses)
     .Map(t => t.MapLeftKey("CourseID")
         .MapRightKey("InstructorID")
         .ToTable("CourseInstructor"));

En este tutorial solo usará la API fluida para la asignación de base de datos que no pueda realizar con atributos. Pero también se puede usar la API fluida para 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. Como se ha mencionado antes, MinimumLength no cambia el esquema, solo aplica una regla de validación del lado cliente y del lado servidor

Algunos desarrolladores prefieren usar la API fluida de forma exclusiva para así mantener las clases de entidad "limpias". Si quiere, puede mezclar atributos y la API fluida, y hay algunas personalizaciones que solo se pueden realizar mediante la API fluida, pero en general el procedimiento recomendado es elegir uno de estos dos enfoques y usarlo de forma constante siempre que sea posible.

Para agregar las nuevas entidades al modelo de datos y realizar la asignación de base de datos que no ha hecho mediante atributos, reemplace el código de DAL\SchoolContext.cs por el código siguiente:

using ContosoUniversity.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace ContosoUniversity.DAL
{
   public class SchoolContext : DbContext
   {
      public DbSet<Course> Courses { get; set; }
      public DbSet<Department> Departments { get; set; }
      public DbSet<Enrollment> Enrollments { get; set; }
      public DbSet<Instructor> Instructors { get; set; }
      public DbSet<Student> Students { get; set; }
      public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
         modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

         modelBuilder.Entity<Course>()
             .HasMany(c => c.Instructors).WithMany(i => i.Courses)
             .Map(t => t.MapLeftKey("CourseID")
                 .MapRightKey("InstructorID")
                 .ToTable("CourseInstructor"));
      }
   }
}

La nueva instrucción del método OnModelCreating configura la tabla de combinación de varios a varios:

  • Para la relación de varios a varios entre las entidades Instructor y Course, el código especifica los nombres de tabla y columna para la tabla de combinación. Code First puede configurar la relación de varios a varios sin este código, pero si no lo llama, obtendrá nombres predeterminados como InstructorInstructorID para la columna InstructorID.

    modelBuilder.Entity<Course>()
        .HasMany(c => c.Instructors).WithMany(i => i.Courses)
        .Map(t => t.MapLeftKey("CourseID")
            .MapRightKey("InstructorID")
            .ToTable("CourseInstructor"));
    

En el código siguiente se proporciona un ejemplo de cómo podría haber usado la API fluida en lugar de atributos para especificar la relación entre las entidades Instructor y OfficeAssignment:

modelBuilder.Entity<Instructor>()
    .HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);

Para obtener información sobre lo que hacen las instrucciones de "API fluida" en segundo plano, vea la entrada de blog de la API fluida.

Inicializa la base de datos con datos de prueba

Reemplace el código del archivo Migrations\Configuration.cs con el código siguiente a fin de proporcionar datos de inicialización para las nuevas entidades que ha creado.

namespace ContosoUniversity.Migrations
{
    using ContosoUniversity.Models;
    using ContosoUniversity.DAL;
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;
    
    internal sealed class Configuration : DbMigrationsConfiguration<SchoolContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(SchoolContext context)
        {
            var students = new List<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") }
            };

            students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s));
            context.SaveChanges();

            var instructors = new List<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") }
            };
            instructors.ForEach(s => context.Instructors.AddOrUpdate(p => p.LastName, s));
            context.SaveChanges();

            var departments = new List<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 }
            };
            departments.ForEach(s => context.Departments.AddOrUpdate(p => p.Name, s));
            context.SaveChanges();

            var courses = new List<Course>
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
            };
            courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
            context.SaveChanges();

            var officeAssignments = new List<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" },
            };
            officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.InstructorID, s));
            context.SaveChanges();

            AddOrUpdateInstructor(context, "Chemistry", "Kapoor");
            AddOrUpdateInstructor(context, "Chemistry", "Harui");
            AddOrUpdateInstructor(context, "Microeconomics", "Zheng");
            AddOrUpdateInstructor(context, "Macroeconomics", "Zheng");

            AddOrUpdateInstructor(context, "Calculus", "Fakhouri");
            AddOrUpdateInstructor(context, "Trigonometry", "Harui");
            AddOrUpdateInstructor(context, "Composition", "Abercrombie");
            AddOrUpdateInstructor(context, "Literature", "Abercrombie");

            context.SaveChanges();

            var enrollments = new List<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();
        }

        void AddOrUpdateInstructor(SchoolContext context, string courseTitle, string instructorName)
        {
            var crs = context.Courses.SingleOrDefault(c => c.Title == courseTitle);
            var inst = crs.Instructors.SingleOrDefault(i => i.LastName == instructorName);
            if (inst == null)
                crs.Instructors.Add(context.Instructors.Single(i => i.LastName == instructorName));
        }
    }
}

Como ha visto en el primer tutorial, la mayor parte de este código simplemente crea objetos de entidad y carga los datos de ejemplo en propiedades según sea necesario para las pruebas. Pero observe cómo se controla la entidad Course, que tiene una relación de varios a varios con la entidad Instructor:

var courses = new List<Course>
{
    new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
      DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
      Instructors = new List<Instructor>() 
    },
    ...
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();

Al crear un objeto Course, inicializa la propiedad de navegación Instructors como una colección vacía mediante el código Instructors = new List<Instructor>(). Esto permite agregar entidadesInstructor relacionadas con Course mediante el método Instructors.Add. Si no hubiera creado una lista vacía, no podría agregar estas relaciones, ya que la propiedad Instructors sería null y no tendría un método Add. También podría agregar la inicialización de lista al constructor.

Agregar una migración

Desde la PMC, escriba el comando add-migration (aún no haga el comando update-database):

add-Migration ComplexDataModel

Si ahora intentara ejecutar el comando update-database (no lo haga todavía), obtendría el error siguiente:

Instrucción ALTER TABLE en conflicto con la restricción FOREIGN KEY "FK_dbo.Course_dbo.Department_DepartmentID". El conflicto ha aparecido en la base de datos "ContosoUniversity", tabla "dbo.Department", columna "DepartmentID".

En ocasiones, al ejecutar migraciones con datos existentes, debe insertar datos de código auxiliar en la base de datos para satisfacer las restricciones de clave externa, como hará ahora. El código generado en el método Up de ComplexDataModel agrega una clave externa DepartmentID que no acepta valores NULL a la tabla Course. Si ya hay filas en la tabla Course cuando se ejecuta el código, se produce un error en la operación AddColumn porque SQL Server no sabe qué valor incluir en la columna que no puede ser NULL. Por tanto, hay que cambiar el código para asignar un valor predeterminado a la nueva columna y crear un departamento de código auxiliar denominado "Temp" para que actúe como el departamento predeterminado. Como resultado, las filas Course existentes estarán relacionadas con el departamento "Temp" después de ejecutar el método Up. Puede relacionarlas con los departamentos correctos en el método Seed.

Edite el archivo <timestamp>_ComplexDataModel.cs, comente la línea de código que agrega la columna DepartmentID a la tabla Course y agregue el código resaltado siguiente (la línea comentada también está resaltada):

CreateTable(
        "dbo.CourseInstructor",
        c => new
            {
                CourseID = c.Int(nullable: false),
                InstructorID = c.Int(nullable: false),
            })
        .PrimaryKey(t => new { t.CourseID, t.InstructorID })
        .ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
        .ForeignKey("dbo.Instructor", t => t.InstructorID, cascadeDelete: true)
        .Index(t => t.CourseID)
        .Index(t => t.InstructorID);

    // Create  a department for course to point to.
    Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
    //  default value for FK points to department created above.
    AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false, defaultValue: 1)); 
    //AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false));

    AlterColumn("dbo.Course", "Title", c => c.String(maxLength: 50));

Cuando se ejecute el método Seed, insertará filas en la tabla Department y relacionará las filas Course existentes con esas filas Department nuevas. Si todavía no ha agregado cursos en la interfaz de usuario, ya no necesitará el departamento "Temp" ni el valor predeterminado en la columna Course.DepartmentID. Para permitir la posibilidad de que alguien haya agregado cursos mediante la aplicación, también querrá actualizar el código del método Seed para asegurarse de que todas las filas Course (no solo las insertadas por ejecuciones anteriores del método Seed) tienen valores DepartmentID válidos antes de quitar el valor predeterminado de la columna y eliminar el departamento "Temp".

Actualizar la base de datos

Una vez que haya terminado de editar el archivo <timestamp>_ComplexDataModel.cs, escriba el comando update-database en la PMC para ejecutar la migración.

update-database

Nota:

Al migrar datos y hacer cambios en el esquema, es posible que se generen otros errores. Si se producen errores de migración que no se pueden resolver, puede cambiar el nombre de la base de datos en la cadena de conexión o eliminar la base de datos. El enfoque más sencillo consiste en cambiar el nombre de la base de datos en el archivo Web.config. En el ejemplo siguiente se muestra el nombre cambiado a CU_Test:

<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;Integrated Security=SSPI;" 
      providerName="System.Data.SqlClient" />

Con una base de datos nueva no hay ningún dato para migrar y es mucho más probable que el comando update-database se complete sin errores. Para obtener instrucciones sobre cómo eliminar la base de datos, consulte Procedimiento para quitar una base de datos de Visual Studio 2012.

Si se produce un error, otra cosa que puede intentar es volver a inicializar la base de datos escribiendo el siguiente comando en la PMC:

update-database -TargetMigration:0

Abra la base de datos en el Explorador de servidores como hizo antes y expanda el nodo Tablas para ver que se han creado todas las tablas. (Si el Explorador de servidores sigue abierto de la vez anterior, haga clic en el botón Actualizar).

Screenshot that shows the Server Explorer window. The Tables folder under School Context is open.

No ha creado una clase de modelo para la tabla CourseInstructor. Como se ha explicado antes, se trata de una tabla de combinación para la relación de varios a varios entre las entidades Instructor y Course.

Haga clic con el botón derecho en la tabla CourseInstructor y seleccione Mostrar datos de tabla para comprobar que tiene datos en ella como resultado de las entidades Instructor que ha agregado a la propiedad de navegación Course.Instructors.

Table_data_in_CourseInstructor_table

Obtención del código

Descargar el proyecto completado

Recursos adicionales

Puede encontrar vínculos a otros recursos de Entity Framework en el Acceso a datos de ASP.NET: recursos recomendados.

Pasos siguientes

En este tutorial ha:

  • Se personalizó el modelo de datos.
  • Se actualizó la entidad Student.
  • Creado la entidad Instructor
  • Creado la entidad OfficeAssignment
  • Se modificó la entidad Course.
  • Se creó la entidad Department.
  • Se modificó la entidad Enrollment.
  • Se agregó código al contexto de base de datos.
  • Inicializado la base de datos con datos de prueba
  • Agregado una migración
  • Actualizado la base de datos

Pase al siguiente artículo para aprender a leer y mostrar datos relacionados que Entity Framework carga en las propiedades de navegación.