0% encontró este documento útil (0 votos)
453 vistas222 páginas

Tutorial - 2. Introducción A Core y Entity Framework Core de ASP

A través de la aplicación web de muestra “Universidad Contoso” aprenderá a crear aplicaciones web ASP.NET Core 2.0 MVC utilizando Entity Framework (EF) Core 2.0 y Visual Studio 2017. La aplicación de muestra es un sitio web para una Universidad ficticia de Contoso. Incluye funciones tales como admisión de estudiantes, creación de cursos y asignaciones de instructores.

Cargado por

soloelectonicos
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
453 vistas222 páginas

Tutorial - 2. Introducción A Core y Entity Framework Core de ASP

A través de la aplicación web de muestra “Universidad Contoso” aprenderá a crear aplicaciones web ASP.NET Core 2.0 MVC utilizando Entity Framework (EF) Core 2.0 y Visual Studio 2017. La aplicación de muestra es un sitio web para una Universidad ficticia de Contoso. Incluye funciones tales como admisión de estudiantes, creación de cursos y asignaciones de instructores.

Cargado por

soloelectonicos
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 222

Introducción a ASP.

NET Core y Entity Framework


Core con Visual Studio
Esta tutorial le enseñará cómo crear aplicaciones Web ASP.NET Core MVC que utilizan Entity
Framework Core para el acceso a datos. Los tutoriales requieren Visual Studio 2017.

1. Introducción a ASP.NET Core MVC y Entity


Framework Core utilizando Visual Studio
Objetivo: Crear una aplicación sencilla que utiliza Entity Framework Core y SQL Server
Express LocalDB para almacenar y mostrar datos.

A través de la aplicación web de muestra “Universidad Contoso” aprenderá a crear


aplicaciones web ASP.NET Core 2.0 MVC utilizando Entity Framework (EF) Core 2.0 y Visual
Studio 2017.

La aplicación de muestra es un sitio web para una Universidad ficticia de Contoso. Incluye
funciones tales como admisión de estudiantes, creación de cursos y asignaciones de
instructores.

Requisitos previos

Instale lo siguiente:

• .NET Core 2.0.0 SDK o posterior. (https://www.microsoft.com/net/core)


• Visual Studio 2017 versión 15.3 o posterior con el ASP.NET y la carga de trabajo Comentado [UdW1]:
de desarrollo web . (https://www.visualstudio.com/es/downloads/) Comentado [UdW2R1]:
La aplicación web de la Universidad Contoso

La aplicación que construirá en este tutorial es un sitio web universitario sencillo.

Los usuarios pueden ver y actualizar la información de los estudiantes, el curso y el


instructor. Estas son algunas de las pantallas que creará.
El estilo de interfaz de usuario de este sitio se ha mantenido similar a lo que se genera por
las plantillas integradas, de modo que el tutorial se puede centrar principalmente en cómo
utilizar el Entity Framework.

Crear una aplicación web ASP.NET Core MVC

Abra Visual Studio y cree un nuevo proyecto web ASP.NET Core C # denominado
"ContosoUniversity".

• En el menú Archivo , seleccione Nuevo> Proyecto .


• En el panel izquierdo, seleccione Instalado> Visual C #> Web .
• Seleccione la plantilla de proyecto Aplicacion web ASP.NET Core (.NET Core).
• Introduzca ContosoUniversity como nombre del proyecto y haga clic en Aceptar .
• Espere a que aparezca el cuadro de diálogo Nueva aplicación Web ASP.NET (.NET
Core)
• Seleccione ASP.NET Core 2.0 y la plantilla de Aplicación Web (Model-View-
Controller) .

Nota: Este tutorial requiere ASP.NET Core 2.0 y EF Core 2.0 o posterior. Asegúrese de
que ASP.NET Core 1.1 no esté seleccionado.

• Asegúrese de que Autenticación está establecida en Sin autenticación .


• Haga clic en Aceptar
Configurar el estilo del sitio

Algunos cambios sencillos configurarán el menú del sitio, el diseño y la página de inicio.

Abra Views/Shared/_Layout.cshtml y realice los siguientes cambios:

• Cambiar cada ocurrencia de "ContosoUniversity" a "Contoso University". Hay tres


ocurrencias.
• Añada entradas de menú
para Estudiantes , Cursos , Instructores y Departamentos y elimine la entrada del
menú Contacto .

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Contoso University</title>

<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet"
href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position"
asp-fallback-test-value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
@Html.Raw(JavaScriptSnippet.FullScript)
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-
target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-
brand">Contoso University</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-
action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-
action="About">About</a></li>

<li><a asp-area="" asp-controller="Students" asp-


action="Index">Students</a></li>
<li><a asp-area="" asp-controller="Courses" asp-
action="Index">Courses</a></li>
<li><a asp-area="" asp-controller="Instructors" asp-
action="Index">Instructors</a></li>

<li><a asp-area="" asp-controller="Departments" asp-


action="Index">Departments</a></li>
</ul>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; 2017 - Contoso University</p>
</footer>
</div>

<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-
K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn &&
window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-
Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>

@RenderSection("Scripts", required: false)


</body>
</html>

En Views/Home/Index.cshtml , reemplace el contenido del archivo con el código siguiente


con el objeto de reemplazar el texto acerca de ASP.NET y MVC con texto acerca de esta
aplicación:

@{
ViewData["Title"] = "Home Page";
}

<div class="jumbotron">
<h1>Contoso University</h1>
</div>
<div class="row">
<div class="col-md-4">
<h2>Welcome to Contoso University</h2>
<p>
Contoso University is a sample application that
demonstrates how to use Entity Framework Core in an
ASP.NET Core MVC web application.
</p>
</div>
<div class="col-md-4">
<h2>Build it from scratch</h2>
<p>You can build the application by following the steps in a series of
tutorials.</p>
<p><a class="btn btn-default" href="https://docs.asp.net/en/latest/data/ef-
mvc/intro.html">See the tutorial &raquo;</a></p>
</div>
<div class="col-md-4">
<h2>Download it</h2>
<p>You can download the completed project from GitHub.</p>
<p><a class="btn btn-default"
href="https://github.com/aspnet/Docs/tree/master/aspnetcore/data/ef-
mvc/intro/samples/cu-final">See project source code &raquo;</a></p>
</div>
</div>

Presione CTRL + F5 para ejecutar el proyecto o elija Debug> Iniciar sin depuración en el
menú de Visual Studio. Verá la página principal con pestañas para las páginas que creará en
estos tutoriales.
Entity Framework Core: Paquetes NuGet

Para agregar soporte de EF Core a un proyecto, instale el proveedor de base de datos al


que desee orientar. Este tutorial utiliza SQL Server y el paquete de proveedor
es Microsoft.EntityFrameworkCore.SqlServer . Este paquete está incluido en
el metapackage Microsoft.AspNetCore.All , por lo que no tiene que instalarlo.

Este paquete y sus dependencias


( Microsoft.EntityFrameworkCore y Microsoft.EntityFrameworkCore.Relational ) proporcionan
soporte de tiempo de ejecución para EF.
Crear el modelo de datos

A continuación, creará clases de entidad para la aplicación Contoso University. Comenzarás


con las tres entidades siguientes.

Hay una relación uno-a-muchos entre las entidades Student y Enrollment , y hay una
relación uno-a-muchos entre las entidades Course y Enrollment . En otras palabras, un
estudiante puede estar matriculado en varios cursos, y un curso puede tener cualquier
número de estudiantes matriculados en él.

En las siguientes secciones crearás una clase para cada una de estas entidades.

La entidad Estudiante

En la carpeta Models , cree un archivo de clase denominada Student.cs y reemplace el


código de plantilla por el código siguiente.

using System;
using System.Collections.Generic;

namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }

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


}
}

La propiedad ID se convertirá en la columna de clave principal de la tabla de base de


datos que corresponde a esta clase. De forma predeterminada, Entity Framework interpreta
una propiedad que se denomina ID o classnameID como la clave principal.

La propiedad Enrollments es una propiedad de navegación. Las propiedades de Navigacion


permiten mantener relación con otras entidades que están relacionadas con esta. En ese
caso, la propiedad Enrollments de una entidad estudiante Student mantendrá relación con
todas las entidades Enrollment que están relacionadas con la entidad Student . En otras
palabras, si la fila de un Student en la base de datos tuviera dos filas de de Enrollment
relacionadas (filas que contienen el valor de clave principal del estudiant en su columna de
clave externa StudentID) la propiedad de navegación de la entidad Enrollments de ese
estudiante contendrá esas dos entidades.

Si una propiedad de navegación puede contener varias entidades (como en las relaciones
uno a muchos, mucho a muchos), su tipo debe ser una lista en la que las entradas se
pueden agregar, eliminar y actualizar, como ICollection<T> . Puede
especificar ICollection<T> o un tipo como List<T> o HashSet<T> . Si
especifica ICollection<T> , EF crea una coleccion HashSet<T> de forma predeterminada.
La Entidad de Inscripción

En la carpeta Models , cree Enrollment.cs y reemplace el código existente con el código


siguiente:

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; }
public Grade? Grade { get; set; }

public Course Course { get; set; }


public Student Student { get; set; }
}
}

La propiedad EnrollmentID será la clave principal; esta entidad usa el patrón classnameID
en vez de unicamente ID como usted vio en la entidad Student . Por lo general, se elige un
único patrón para utilizarlo en todo el modelo de datos. Aquí, la variación ilustra que
puede utilizar cualquiera de los patrones. Posteriormente , verá cómo el uso de ID sin
classname facilita la implementación de la herencia en el modelo de datos.

La propiedad Grade es del tipo enum . El signo de interrogación después de la declaración


de tipo Grade indica que la propiedad Grade es anulable. Nulo significa que no se conoce o
no se ha asignado todavía (es distinto de vacio).
La propiedad StudentID es una clave externa y la propiedad de navegación
correspondiente es Student . Una entidad Enrollment está asociada a una única entidad
Student , por lo que la propiedad sólo puede contener una sola entidad Student (a
diferencia de la propiedad de navegación Student.Enrollments que vio anteriormente, que
puede contener varias entidades Enrollment ).

La propiedad CourseID es una clave externa y la propiedad de navegación correspondiente


es Course . Una entidad Enrollment está asociada a una entidad Course .

Entity Framework interpreta una propiedad como una propiedad de clave externa si se
nombra de la forma <navigation property name><primary key property name> (por
ejemplo, StudentID para la propiedad de navegación Student , ya que la clave principal de la
entidad Student es ID ). Las propiedades de clave externa también pueden nombrarse
simplemente como <primary key property name> (por ejemplo, CourseID puesto que la
clave principal de la entidad Course es CourseID ).

La entidad del curso

En la carpeta Models , cree Course.cs y reemplace el código existente con el código


siguiente:

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

namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }

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


}
}

La propiedad Enrollments es una propiedad de navegación. Una entidad Course puede


estar relacionada con cualquier número de entidades Enrollment .

El atributo DatabaseGenerated permite introducir la clave principal manualmente para la


entidad Course en lugar de que la base de datos la genere automaticamente.

Crear el contexto de la base de datos

La clase principal que coordina la funcionalidad de Entity Framework para un modelo de


datos dado es la clase de contexto de la base de datos. Cree esta clase derivandola de la
clase Microsoft.EntityFrameworkCore.DbContext . En su código, especifique qué entidades se
incluyen en el modelo de datos. También puede personalizar el comportamiento de Entity
Framework. En este proyecto, se nombra la clase SchoolContext .

En la carpeta del proyecto, cree una carpeta denominada Data .

En la carpeta Data, cree un archivo de clase nuevo denominado SchoolContext.cs y


reemplace el código de plantilla por el código siguiente:

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; }
}
}
Este código crea una propiedad DbSet para cada conjunto de entidades. En la terminología
de Entity Framework, un conjunto de entidades corresponde típicamente a una tabla de la
base de datos y una entidad corresponde a una fila, a un registro de la tabla.

Se podrían haber omitido las declaraciones de DbSet<Enrollment> y DbSet<Course> y


funcionaría igualmente. Entity Framework los incluiría implícitamente porque la
entidad Student hace referencia a la entidad Enrollment y la entidad Enrollment hace
referencia a la entidad Course .

Cuando se crea la base de datos, EF crea tablas que tienen nombres iguales que los
nombres de propiedad DbSet . Los nombres de propiedad para las colecciones suelen ser en
plural (Estudiantes en lugar de Estudiante), pero no hay consenso en los desarrolladores en
si los nombres de las tablas deben ser pluralizados o no. En nuestro caso, anularemos el
comportamiento por defecto de ser pluralizados, y los nombres de las tablas serán en
singulares en el DbContext. Para ello, agregue el código resaltado siguiente después de la
última propiedad DbSet.

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

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");

}
}
}
Registrar el contexto con la inyección de dependencia

ASP.NET Core implementa la inyección de dependencia de forma predeterminada. Los


servicios (como el contexto de la base de datos EF) se registran con la inyección de
dependencia durante el inicio de la aplicación. Los componentes que requieren estos
servicios (como los controladores MVC) se proporcionan a estos servicios a través de
parámetros de constructor.

Para registrar SchoolContext como un servicio, abra Startup.cs y agregue las líneas resaltadas
al método ConfigureServices .

public void ConfigureServices(IServiceCollection services)


{

services.AddDbContext<SchoolContext>(options =>

options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddMvc();
}

El nombre de la cadena de conexión se pasa al contexto llamando a un método en un


objeto DbContextOptionsBuilder . Para el desarrollo local, el sistema de configuración de
ASP.NET Core lee la cadena de conexión del archivo appsettings.json .

Agregue sentencias using para los espacios de nombre


ContosoUniversity.Data y Microsoft.EntityFrameworkCore y a continuación, compile el
proyecto.

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;

Abra el archivo appsettings.json y agregue una cadena de conexión como se muestra en el


ejemplo siguiente.

"ConnectionStrings": {
"DefaultConnection":
"Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity1;Trusted_Connection=True;Multipl
eActiveResultSets=true"

},
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
}
}

SQL Server Express LocalDB

La cadena de conexión especifica una base de datos SQL Server LocalDB. LocalDB es una
versión ligera del motor de base de datos SQL Server Express y está diseñado para el
desarrollo de aplicaciones y no para su uso en la producción. LocalDB se inicia a petición y
se ejecuta en modo de usuario, por lo que no hay una configuración compleja. De forma
predeterminada, LocalDB crea archivos de base de datos .mdf en el directorio
C:/Users/<user> .

Agregar código para inicializar la base de datos con datos de prueba

Entity Framework creará una base de datos vacía para usted. En esta sección, se escribe un
método que se llama después de que se crea la base de datos con el fin de rellenarlo con
datos de prueba.

Aquí utilizará el método EnsureCreated para crear automáticamente la base de datos. Más
adelante verá cómo manejar los cambios de un modelo mediante el uso de Migraciones
para cambiar el esquema de la base de datos en lugar de eliminar y volver a crear la base
de datos.

En la carpeta Data , cree un archivo de clase nuevo denominado DbInitializer.cs y reemplace


el código de plantilla por el código siguiente, lo que hace que se cree una base de datos
cuando sea necesario y cargue los datos de prueba en la nueva base de datos.

using ContosoUniversity.Models;
using System;
using System.Linq;
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("2005-
09-01")},
new
Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2002-
09-01")},
new
Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2003-09-
01")},
new
Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2002-
09-01")},
new
Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002-09-01")},
new
Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2001-09-
01")},
new
Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2003-09-
01")},
new
Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2005-09-
01")}
};
foreach (Student s in students)
{
context.Students.Add(s);
}
context.SaveChanges();

var courses = new Course[]


{
new Course{CourseID=1050,Title="Chemistry",Credits=3},
new Course{CourseID=4022,Title="Microeconomics",Credits=3},
new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
new Course{CourseID=1045,Title="Calculus",Credits=4},
new Course{CourseID=3141,Title="Trigonometry",Credits=4},
new Course{CourseID=2021,Title="Composition",Credits=3},
new Course{CourseID=2042,Title="Literature",Credits=4}
};
foreach (Course c in courses)
{
context.Courses.Add(c);
}
context.SaveChanges();

var enrollments = new Enrollment[]


{
new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
new Enrollment{StudentID=3,CourseID=1050},
new Enrollment{StudentID=4,CourseID=1050},
new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
new Enrollment{StudentID=6,CourseID=1045},
new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
};
foreach (Enrollment e in enrollments)
{
context.Enrollments.Add(e);
}
context.SaveChanges();
}
}
}

El código comprueba si hay estudiantes en la base de datos, y si no, se supone que la base
de datos es nueva y necesita ser rellenada con datos de prueba. Se cargan datos de prueba
en arrays en lugar de colecciones List<T> para optimizar el rendimiento.

En Program.cs , modifique el método Main para hacer lo siguiente al inicio de la aplicación:


• Obtener una instancia de contexto de base de datos del contenedor de inyección de
dependencia.
• Llame al método de semilla, pasándole el contexto.
• Elimine el contexto cuando se realiza el método de semilla.

public static void Main(string[] args)


{

var host = BuildWebHost(args);

using (var scope = host.Services.CreateScope())


{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<SchoolContext>();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
}
}

host.Run();
}

Añadir las siguiente declaraciones using :

using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Data;

Le recomendamos que utilice el método Configure sólo para configurar la canalización de


una solicitud. El código de inicio de la aplicación pertenece al método Main .

Ahora la primera vez que ejecute la aplicación, la base de datos se creará y se sembrará con
datos de prueba. Cada vez que cambie su modelo de datos, puede eliminar la base de
datos, actualizar su método de semilla y empezar de nuevo con una nueva base de datos de
la misma manera. En tutoriales posteriores, verá cómo modificar la base de datos cuando
cambia el modelo de datos, sin eliminarla ni volver a crearla.
Crear un controlador y vistas

A continuación, utilizará el motor de andamios (Scaffold) en Visual Studio para agregar un


controlador MVC y vistas que utilizarán EF para consultar y guardar datos.

La creación automática de métodos de acción CRUD y vistas se conoce como andamiaje. El


andamiaje difiere de la generación de código en que el código de andamio es un punto de
partida que puede modificar para adaptarse a sus propios requisitos, mientras que
típicamente no modifica el código generado. Cuando necesita personalizar el código
generado, utiliza clases parciales o regenera el código cuando cambian las cosas.

• Haga clic con el botón secundario en la carpeta Controllers en el Explorador de


soluciones y seleccione Agregar> Nuevo elemento de Scaffold .
• En el diálogo Add MVC Dependencies , seleccione Minimal Dependencies , y
seleccione Add .

Visual Studio añade las dependencias necesarias para scaffold un controlador. El único
cambio en el archivo de proyecto es la adición del
paquete Microsoft.VisualStudio.Web.CodeGeneration.Design .

Se crea un archivo ScaffoldingReadMe.txt que puede eliminar.

• Una vez más, haga clic con el botón derecho en la carpeta Controllers en
el Explorador de soluciones y seleccione Agregar> Nuevo elemento de scaffold .
• En el cuadro de diálogo Add Scaffold :
o Seleccione Controlador MVC con vistas, que usan Entity Framework .
o Haga clic en Agregar .
• En el cuadro de diálogo Agregar controlador :
o En la clase Model, seleccione Student .
o En la clase de contexto Data, seleccione SchoolContext .
o Acepte el valor predeterminado StudentsController como el nombre.
o Haga clic en Agregar .
Al hacer clic en Agregar , el motor de andamios de Visual Studio crea
un archivo StudentsController.cs y un conjunto de vistas ( archivos .cshtml ) que
funcionan con el controlador.

(El motor de andamios también puede crear el contexto de la base de datos para usted si
no lo crea manualmente primero como lo hizo anteriormente). Puede especificar una nueva
clase de contexto en el cuadro Agregar controlador haciendo clic en el signo más a la
derecha de Clase de contexto de datos, Visual Studio creará su clase DbContext así como
el controlador y las vistas.

Observe que el controlador toma un parámetro SchoolContext en el constructor.

namespace ContosoUniversity.Controllers
{
public class StudentsController : Controller
{
private readonly SchoolContext _context;

public StudentsController(SchoolContext context)

{
_context = context;
}

La inyección de dependencia de ASP.NET se encargará de pasar una instancia


de SchoolContext en el controlador. Ha sido configurado anteriormente en
el archivo Startup.cs .

El controlador contiene un método de acción Index , que muestra a todos los estudiantes
en la base de datos. El método obtiene una lista de estudiantes de la entidad Student
establecida leyendo la propiedad Students de la instancia de contexto de la base de datos:

public async Task<IActionResult> Index()


{
return View(await _context.Students.ToListAsync());
}

Aprenderá sobre los elementos de programación asíncronos en este código más adelante
en el tutorial.

La vista Views/Students/Index.cshtml muestra esta lista en una tabla:

@model IEnumerable<ContosoUniversity.Models.Student>

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.LastName)
</th>
<th>
@Html.DisplayNameFor(model => model.FirstMidName)
</th>
<th>
@Html.DisplayNameFor(model => model.EnrollmentDate)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Presione CTRL + F5 para ejecutar el proyecto o elija Debug> Iniciar sin depuración en el
menú.

Haga clic en la ficha Estudiantes para ver los datos de prueba que el
método DbInitializer.Initialize insertó. Dependiendo de cuán estrecha sea la ventana
del navegador, verás el enlace Student de la pestaña en la parte superior de la página o
tendrás que hacer clic en el icono de navegación en la esquina superior derecha para ver el
enlace.
Ver la base de datos

Cuando inició la aplicación, el método DbInitializer.Initialize llama EnsureCreated . EF


vio que no había ninguna base de datos, por lo que creó una, a continuación, el resto del
código del método Initialize inserta los datos. Puede utilizar el Explorador de objetos
de SQL Server (SSOX) para ver la base de datos en Visual Studio.

Cierre el navegador.

Si la ventana SSOX no está abierta, selecciónela desde el menú Ver en Visual Studio.

En SSOX, haga clic en (localdb) \ MSSQLLocalDB> Bases de datos y a continuación,


haga clic en la entrada para el nombre de base de datos que está en la cadena de conexión
en su archivo appsettings.json .

Expanda el nodo Tablas para ver las tablas en su base de datos.

Haga clic con el botón secundario en la tabla Student y haga clic en Ver datos para ver las
columnas que se crearon y las filas que se insertaron en la tabla.
Los archivos de base de datos .mdf y .ldf se encuentran en la carpeta C: \Users
<yourusername> .

Debido a que está llamando al método de inicialización EnsureCreated que se ejecuta en el


inicio de la aplicación, ahora podría realizar un cambio en la clase Student , eliminar la base
de datos, volver a ejecutar la aplicación y la base de datos se volvería a crear
automáticamente para que coincida con su cambio. Por ejemplo, si agrega una
propiedad EmailAddress a la clase Student , verá una nueva columna EmailAddress en la
tabla creada nuevamente.

Convenciones

La cantidad de código que tenía que escribir para que Entity Framework pueda crear una
base de datos completa para usted es mínima debido al uso de convenciones o
suposiciones que hace el Entity Framework.

• Los nombres de las propiedades DbSet se utilizan como nombres de tablas. Para
entidades no referenciadas por una propiedad DbSet , los nombres de clase de entidad
se utilizan como nombres de tablas.
• Los nombres de propiedad de entidad se utilizan para nombres de columna.
• Las propiedades de entidad que se denominan ID o ID de clase se reconocen como
propiedades de clave principal.
• Una propiedad se interpreta como una propiedad de clave externa si se
denomina (por ejemplo, StudentID para la propiedad de navegación Student ya que la
clave principal de la entidad Student es ID ).

El comportamiento convencional puede ser anulado. Por ejemplo, puede especificar


explícitamente nombres de tablas, como se vio anteriormente en este tutorial. Y puede
establecer nombres de columnas y establecer cualquier propiedad como clave principal o
clave ajena, como verá mas adelante.
Código asíncrono

La programación asíncrona es el modo predeterminado para ASP.NET Core y EF Core. Un


servidor web tiene un número limitado de subprocesos disponibles y, en situaciones de alta
carga, todos los subprocesos disponibles podrían estar en uso. Cuando esto sucede, el
servidor no puede procesar nuevas solicitudes hasta que se liberen los subprocesos. Con
código síncrono, muchos subprocesos pueden estar atados mientras que no están
realmente haciendo ningún trabajo porque están esperando a completar una operación de
E/S . Con el código asíncrono, cuando un proceso está esperando que las E/S se completen,
su subproceso se libera para que el servidor lo utilice para procesar otras solicitudes. Como
resultado, el código asíncrono permite que los recursos del servidor se utilicen de manera
más eficiente, y el servidor está habilitado para manejar más tráfico sin demoras.

El código asíncrono introduce una pequeña cantidad de sobrecarga en tiempo de


ejecución, pero para situaciones de bajo tráfico el rendimiento es insignificante, mientras
que para situaciones de alto tráfico, la mejora de rendimiento potencial es sustancial.

En el código siguiente, la palabra clave async , el valor devuelto Task<T> , la palabra


clave await y el método ToListAsync hacen que el código se ejecute asincrónicamente.

public async Task<IActionResult> Index()


{
return View(await _context.Students.ToListAsync());
}

• La palabra clave async le dice al compilador que genere devoluciones de llamada para
partes del cuerpo del método y que cree automáticamente el objeto
devuelto Task<IActionResult> .
• El tipo de retorno Task<IActionResult> representa el trabajo en curso con un resultado
de tipo IActionResult .
• La palabra clave await hace que el compilador divida el método en dos partes. La
primera parte termina con la operación que se inicia de forma asíncrona. La segunda
parte se pone en un método de devolución de llamada que se llama cuando se
completa la operación.
• ToListAsync es la versión asíncrona del método de extensión ToList .

Algunas cosas que debe tener en cuenta cuando está escribiendo código asincrónico que
utiliza Entity Framework:

• Sólo las sentencias que hacen que las consultas o comandos se envíen a la base de
datos se ejecutan de forma asíncrona. Esto incluye, por
ejemplo, ToListAsync , SingleOrDefaultAsync , y SaveChangesAsync . No incluye, por
ejemplo, declaraciones que sólo cambian un IQueryable , como var students =
context.Students.Where(s => s.LastName == "Davolio") .
• Un contexto EF no es seguro de subproceso: no intente realizar varias operaciones en
paralelo. Cuando llame a cualquier método EF asíncrono, utilice siempre la palabra
clave await .
• Si desea aprovechar los beneficios de rendimiento del código asíncrono, asegúrese de
que los paquetes de biblioteca que esté utilizando (como para la paginación), también
utilicen programación asíncrona si llaman a cualquier método de Entity Framework
que haga que las consultas se envíen al base de datos.
Crear, leer, actualizar y eliminar - EF Core con
ASP.NET Core MVC tutorial (2 of 10)
Objetivo: Personalizar el código CRUD (crear, leer, actualizar, eliminar) que el
andamio MVC crea automáticamente para usted en controladores y vistas.

Nota

Es una práctica común implementar el patrón de repositorio para crear una capa de
abstracción entre su controlador y la capa de acceso a datos. Para mantener estos tutoriales
sencillos y centrados en enseñar cómo utilizar el Entity Framework en sí, no utilizan
repositorios.

En este tutorial, trabajará con las siguientes páginas web:


Personalizar la página Detalles

El código de andamio (scaffold) para la página de índice de estudiantes dejó fuera la


propiedad Enrollments , porque esa propiedad contiene una colección. En
la página Details , mostrará el contenido de la colección en una tabla HTML.

En Controllers/StudentsController.cs , el método de acción de la vista Details utiliza el


método SingleOrDefaultAsync para recuperar una sola entidad Student . Agregue el código
que llama Include . ThenInclude , y los métodos AsNoTracking , como se muestra en el
siguiente código resaltado.
public async Task<IActionResult> Details(int? id)

if (id == null)

return NotFound();

var student = await _context.Students


.Include(s => s.Enrollments)
.ThenInclude(e => e.Course)
.AsNoTracking().SingleOrDefaultAsync(m => m.ID == id);

if (student == null)
{
return NotFound();
}

return View(student);

Los métodos Include y ThenInclude hacen que el contexto cargue la propiedad de


navegación Student.Enrollments y dentro de cada inscripción la propiedad de
navegación Enrollment.Course .

El método AsNoTracking mejora el rendimiento en escenarios en los que las entidades


devueltas no se actualizarán en la vida útil del contexto actual. Aprenderá más
sobre AsNoTracking al final de este capitulo.
Datos de la ruta

El valor clave que se pasa al método Details proviene de los datos de ruta . Los datos de
ruta son datos que el Model Binder encuentra en el segmento de la URL. Por ejemplo, la ruta
predeterminada especifica los segmentos, controlador, action e id:

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

En la siguiente URL, la ruta predeterminada asigna Instructor al controlador, Index como la


acción y 1 como id; estos son valores de datos de ruta.

http://localhost:1230/Instructor/Index/1?courseID=2021

La última parte de la URL ("? CourseID = 2021") es un valor de cadena de consulta. El Model
Binder también pasará el valor ID al parámetro id del método Details si lo pasara como
un valor de la cadena de consulta, como la siguiente:

http://localhost:1230/Instructor/Index?id=1&CourseID=2021

En la página de índice, las direcciones URL de hipervínculo se crean mediante instrucciones


de ayudante de etiqueta (helper statement) en la vista de Razor. En el código Razor
siguiente, el parámetro id coincide con la ruta predeterminada, por lo que id se agrega a
los datos de la ruta.

<a asp-action="Edit" asp-route-id="@item.ID">Edit</a>

Esto genera el siguiente HTML cuando item.ID es 6 :

<a href="/Students/Edit/6">Edit</a>

En el siguiente código Razor, studentID no coincide con un parámetro en la ruta


predeterminada, por lo que se agrega como una cadena de consulta.

<a asp-action="Edit" asp-route-studentID="@item.ID">Edit</a>

Esto genera el siguiente HTML cuando item.ID es :


<a href="/Students/Edit?studentID=6">Edit</a>

Agregar inscripciones a la vista Details

Abra Views/Students/ Details.cshtml . Cada campo se visualiza utilizando los helpers


DisplayNameFor y DisplayFor , como se muestra en el siguiente ejemplo:

<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.LastName)
</dt>
<dd>
@Html.DisplayFor(model => model.LastName)
</dd>

</dl>

Después del último campo e inmediatamente antes del cierre de la etiqueta </dl> ,
agregue el siguiente código para mostrar una lista de matrículas:

<dt>
@Html.DisplayNameFor(model => model.Enrollments)
</dt>
<dd>
<table class="table">
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</dd>
Si la sangría de código es incorrecta después de pegar el código, presione CTRL-KD para
corregirlo.

Este código recorre las entidades de la propiedad Enrollments de navegación. Para cada
inscripción, muestra el título del curso y el grado. El título del curso se recupera de la
entidad del curso que se almacena en la propiedad de navegacion Course de la entidad
Enrollments.

Ejecute la aplicación, seleccione la ficha Estudiantes y haga clic en el enlace Detalles de un


estudiante. Verá la lista de cursos y calificaciones para el estudiante seleccionado:

Actualizar la página Crear

En StudentsController.cs , modifique el método HttpPost Create agregando un bloque try-


catch y quitando ID del atributo Bind .

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("EnrollmentDate,FirstMidName,LastName")] Student student)

try

{
if (ModelState.IsValid)
{
_context.Add(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}

}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists " +
"see your system administrator.");

}
return View(student);
}

Este código agrega la entidad Estudiante creada por el vinculador de modelo (Model
Binder) ASP.NET MVC al conjunto de entidad Estudiantes y, a continuación, guarda los
cambios en la base de datos. (Model Binder se refiere a la funcionalidad de ASP.NET MVC
que facilita el trabajo con los datos enviados por un formulario, un encuadernador de
modelo convierte los valores de formulario publicados en tipos CLR y los pasa al método de
acción en parámetros. En este caso, el vinculador de modelos instancia una entidad de
estudiante para usted utilizando valores de propiedad de la colección de formularios.)

Se eliminó ID del atributo Bind porque ID es el valor de clave principal que SQL Server
establecerá automáticamente cuando se inserte la fila. La entrada del usuario no establece
el valor ID.

A parte del atributo Bind , el bloque try-catch es el único cambio que ha hecho en el
código de andamios. Si una excepción que se deriva de DbUpdateException se captura
mientras se guardan los cambios, se muestra un mensaje de error genérico. las
excepciones DbUpdateException son a veces causadas por algo externo a la aplicación en
lugar de un error de programación, por lo que se recomienda al usuario que lo intente de
nuevo. Aunque no se implementó en este caso, una aplicación en producción de calidad
registraría la excepción.

El atributo ValidateAntiForgeryToken ayuda a prevenir ataques de falsificación de


solicitudes entre sitios (CSRF). El token se inyecta automáticamente en la vista por
el FormTagHelper y se incluye cuando el formulario es enviado por el usuario. El token es
validado por el atricuto ValidateAntiForgeryToken .

Nota de seguridad sobre la sobreposición

El atricuto Bind que el código de andamios incluye en el método Create es una forma de
proteger contra la sobreposición en crear escenarios. Por ejemplo, suponga que la entidad
Estudiante incluye una propiedad Secret que no desea que esta página web establezca.

public class Student


{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public string Secret { get; set; }
}

Incluso si usted no tiene un campo Secret en la página web, un hacker podría usar una
herramienta como Fiddler, o escribir algunos JavaScript, para publicar el valor Secret del
formulario. Sin el atricuto Bind que limita los campos que el Model Binder utiliza cuando
crea una instancia de estudiante, el Model Binder recogerá ese valor de Secret del
formulario y lo utilizará para crear la instancia de entidad la Estudiante. Entonces, cualquier
valor que el hacker especifique para el campo Secret del formulario se actualizaría en su
base de datos. La siguiente imagen muestra la herramienta Fiddler que agrega el
campo Secret (con el valor "OverPost") a los valores de formulario publicados.
El valor "OverPost" se agregará con éxito a la propiedad Secret de la fila insertada, aunque
nunca pensó que la página web pudiera establecer esa propiedad.

Puede evitar la sobreposición en escenarios de edición leyendo primero la entidad de la


base de datos y luego llamando TryUpdateModel , pasando una lista de propiedades
permitida explícita. Ese es el método utilizado en estos tutoriales.

Una forma alternativa de evitar la sobreposición que es preferida por muchos


desarrolladores es usar modelos de vista en lugar de clases de entidad conel Model
Binder. Incluya solo las propiedades que desea actualizar en el modelo de vista. Una vez
que haya finalizado el Model Binder MVC, copie las propiedades del modelo de vista en la
instancia de entidad, utilizando opcionalmente una herramienta como
AutoMapper. Utilíce _context.Entry en la instancia de entidad para establecer su estado
y Unchanged , a continuación, establezca Property("PropertyName").IsModified a true en
cada propiedad de entidad que se incluye en el modelo de vista. Este método funciona
tanto en la edición como en la creación de escenarios.

Pruebe la página Crear

El código de Views/Students/Create.cshtml utiliza label , input y span (para mensajes de


validación) ayudantes de etiquetas para cada campo.
Ejecute la página seleccionando la ficha Estudiantes y haciendo clic en Crear nuevo .

Introduzca los nombres y una fecha. Intenta introducir una fecha no válida si tu navegador
te permite hacerlo. (Algunos navegadores le obligan a utilizar un selector de fechas.) A
continuación, haga clic en Crear para ver el mensaje de error.

Esta es la validación del lado del servidor que se obtiene de forma predeterminada; en un
tutorial posterior verá cómo agregar atributos que generarán código para la validación del
cliente también. El siguiente código resaltado muestra la verificación de validación del
modelo en el método Create .

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
{
try
{
if (ModelState.IsValid)
{
_context.Add(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists " +
"see your system administrator.");
}
return View(student);
}

Cambie la fecha a un valor válido y haga clic en Crear para ver al nuevo estudiante aparecer
en la página de Index .

Actualizar la página Editar

En StudentController.cs , el método HttpGet Edit (el que no tiene el atributo HttpPost )


utiliza el método SingleOrDefaultAsync para recuperar la entidad Estudiante seleccionada,
como se vio en el método Details . No es necesario cambiar este método.

Código recomendado para HttpPost Editar: Leer y actualizar

Reemplace el método de acción HttpPost Editar con el código siguiente.

[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
if (id == null)
{
return NotFound();
}
var studentToUpdate = await _context.Students.SingleOrDefaultAsync(s => s.ID == id);
if (await TryUpdateModelAsync<Student>(
studentToUpdate,
"",
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
try
{
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
}
return View(studentToUpdate);
}

Estos cambios implementan una mejor práctica de seguridad para evitar la sobreposición. El
scaffolder generó un atributo Bind y añadió la entidad creada por el Model Binder al
conjunto de entidades con un flag Modified . Este código no se recomienda para muchos
escenarios porque el atributo Bind borra los datos preexistentes en campos que no
aparecen en el parámetro Include .

El nuevo código lee la entidad existente y llama TryUpdateModel para actualizar campos en
la entidad recuperada basándose en la entrada del usuario en los datos de formulario
publicados . El seguimiento automático de cambios de Entity Framework establece el
indicador Modified en los campos que se cambian por la entrada de formulario. Cuando se
llama al método SaveChanges , Entity Framework crea instrucciones SQL para actualizar la fila
de la base de datos. Los conflictos de simultaneidad se ignoran y sólo se actualizan en la
base de datos las columnas de tabla actualizadas por el usuario.

Como práctica recomendada para evitar la sobreposición, los campos que desea que se
puedan actualizar mediante la página Edit aparecen en la lista de los
parámetros TryUpdateModel . (La cadena vacía que precede a la lista de campos en la lista de
parámetros es para un prefijo para usar con los nombres de los campos de formulario.)
Actualmente, no hay campos adicionales que esté protegiendo, sino que listando los
campos que desea que el Model Binder vincule asegura que si agrega campos al modelo de
datos en el futuro, se protegerán automáticamente hasta que los agregue explícitamente
aquí.
Como resultado de estos cambios, la firma de método del método HttpPost Edit es el
mismo que el método HttpGet Edit ; por lo que ha cambiado el nombre del
método EditPost .

Código alternativo para HttpPost Editar: Crear y adjuntar

El código de edición HttpPost recomendado asegura que sólo las columnas modificadas se
actualizan y conserva los datos en las propiedades que no desea que se incluyan para el
enlace del modelo. Sin embargo, el enfoque de lectura primero requiere una lectura de
base de datos adicional y puede resultar en código más complejo para manejar conflictos
de simultaneidad. Una alternativa es adjuntar una entidad creada por el Model Binder al
contexto EF y marcarlo como modificado. (No actualice su proyecto con este código, solo se
muestra para ilustrar un enfoque opcional).

public async Task<IActionResult> Edit(int id,


[Bind("ID,EnrollmentDate,FirstMidName,LastName")] Student student)
{
if (id != student.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
}
return View(student);
}

Puede utilizar este enfoque cuando la interfaz de usuario de la página web incluye todos los
campos de la entidad y puede actualizar cualquiera de ellos.

El código de andamios utiliza el método create-and-attach pero sólo detecta


excepcione DbUpdateConcurrencyException y devuelve 404 códigos de error. El ejemplo
mostrado captura cualquier excepción de actualización de base de datos y muestra un
mensaje de error.

Estados de Entidad

El contexto de la base de datos controla si las entidades en la memoria están sincronizadas


con sus filas correspondientes en la base de datos y esta información determina qué sucede
cuando llama al método SaveChanges . Por ejemplo, cuando pasa una nueva entidad al
método Add , el estado de esa entidad se establece a Added . A continuación, al llamar al
método SaveChanges , el contexto de la base de datos emite un comando SQL INSERT.

Una entidad puede estar en uno de los siguientes estados:

• Added . La entidad aún no existe en la base de datos. El método SaveChanges emite una
instrucción INSERT.
• Unchanged . El método SaveChanges no hace nada con esta entidad. Cuando se lee
una entidad de la base de datos, la entidad comienza con este estado.
• Modified . Se han modificado algunos o todos los valores de propiedad de la
entidad. El método SaveChanges emite una instrucción UPDATE.
• Deleted . Se ha marcado la entidad para su eliminación. El método SaveChanges emite
una instrucción DELETE.
• Detached . La entidad no está siendo rastreada por el contexto de la base de datos.

En una aplicación de escritorio, los cambios de estado suelen establecerse


automáticamente. Lea una entidad y realice cambios en algunos de sus valores de
propiedad. Esto hace que su estado de entidad cambie automáticamente a Modified . A
continuación, cuando llama SaveChanges , el Entity Framework genera una instrucción SQL
UPDATE que actualiza sólo las propiedades reales que cambió.

En una aplicación web, el DbContext que inicialmente lee una entidad y muestra sus datos a
editar, se elimina después de que se haya representado una página. Cuando Edit se llama
al método de acción HttpPost, se realiza una nueva solicitud web y se tiene una nueva
instancia de DbContext . Si vuelve a leer la entidad en ese nuevo contexto, simula el
procesamiento de escritorio.

Pero si no desea realizar la operación de lectura adicional, debe utilizar el objeto de entidad
creado por el Model Bilder. La forma más sencilla de hacerlo es establecer el estado de la
entidad en Modified como se hace en el código HttpPost Edit alternativo mostrado
anteriormente. Cuando llame a SaveChanges , Entity Framework actualiza todas las columnas
de la fila de la base de datos, ya que el contexto no tiene forma de saber qué propiedades
cambió.
Si desea evitar el método de lectura primero, pero también desea que la instrucción SQL
UPDATE actualice sólo los campos que el usuario ha cambiado realmente, el código es más
complejo. Debe guardar los valores originales de alguna manera (como mediante campos
ocultos) para que estén disponibles cuando se llama al método HttpPost Edit . A
continuación, puede crear una entidad Estudiante utilizando los valores originales, llamar al
método Attach con esa versión original de la entidad, actualizar los valores de la entidad a
los nuevos valores y, a continuación, llamar SaveChanges .

Prueba la página Editar

Ejecute la aplicación y seleccione la ficha Students, a continuación, haga clic en


un hipervínculo Edit .

Cambie algunos de los datos y haga clic en Save . Se abrirá la página Index y verá los datos
modificados.
Actualizar la página Eliminar

En StudentController.cs , el código de plantilla para el método HttpGet Delete utiliza el


método SingleOrDefaultAsync para recuperar la entidad Estudiante seleccionada, como se
vio en los métodos Details y Edit. Sin embargo, para implementar un mensaje de error
personalizado cuando falla la llamada a SaveChanges , agregaremos alguna funcionalidad a
este método y a su vista correspondiente.

Como se vio para las operaciones de actualización y creación, las operaciones de


eliminación requieren dos métodos de acción. El método que se llama en respuesta a una
solicitud GET muestra una vista que da al usuario la oportunidad de aceptar o cancelar la
operación de eliminación. Si el usuario la acepta, se crea una solicitud POST. Cuando esto
sucede, se llama al método HttpPost Delete y, a continuación, ese método realiza
realmente la operación de eliminación.

Agregue un bloque try-catch al método HttpPost Delete para manejar cualquier error que
pueda ocurrir cuando se actualiza la base de datos. Si se produce un error, el método
HttpPost Delete llama al método HttpGet Delete, pasándole un parámetro que indica que
se ha producido un error. El método HttpGet Delete a continuación, vuelve a mostrar la
página de confirmación junto con el mensaje de error, dando al usuario la oportunidad de
cancelar o volver a intentarlo.

Reemplace el método de acción HttpGet Delete con el código siguiente, que administra el
informe de errores.

public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)

{
if (id == null)
{
return NotFound();
}

var student = await _context.Students


.AsNoTracking()
.SingleOrDefaultAsync(m => m.ID == id);
if (student == null)
{
return NotFound();
}

if (saveChangesError.GetValueOrDefault())
{
ViewData["ErrorMessage"] =
"Delete failed. Try again, and if the problem persists " +
"see your system administrator.";

return View(student);
}

Este código acepta un parámetro opcional que indica si el método se llamó después de un
error al guardar los cambios. Este parámetro es false cuando se llama al
método HttpGet Delete sin un error anterior. Cuando se llama por el
método HttpPost Delete en respuesta a un error de actualización de base de datos, el
parámetro es true y se pasa un mensaje de error a la vista.

El enfoque de primera lectura de HttpPost Delete

Reemplace el método de acción HttpPost Delete (denominado DeleteConfirmed ) con el


código siguiente, que realiza la operación de eliminación real y detecta errores de
actualización de la base de datos.

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var student = await _context.Students
.AsNoTracking()
.SingleOrDefaultAsync(m => m.ID == id);

if (student == null)
{
return RedirectToAction(nameof(Index));

try

{
_context.Students.Remove(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));

}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });

}
}

Este código recupera la entidad seleccionada y luego llama al método Remove para
establecer el estado de la entidad Deleted . Cuando SaveChanges se llama, se genera un
comando SQL DELETE.

El método create-and-attach para HttpPost Delete

Si la mejora del rendimiento en una aplicación de alto volumen es una prioridad, podría
evitar una consulta SQL innecesaria instanciando una entidad Estudiante utilizando sólo el
valor de clave principal y estableciendo a continuación el estado de entidad Deleted . Eso es
todo lo que el Entity Framework necesita para eliminar la entidad. (No ponga este código
en su proyecto, está aquí sólo para ilustrar una alternativa.)

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
try
{

Student studentToDelete = new Student() { ID = id };

_context.Entry(studentToDelete).State = EntityState.Deleted;
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true
});
}
}

Si la entidad tiene datos relacionados que también deben eliminarse, asegúrese de que la
eliminación en cascada está configurada en la base de datos. Con este enfoque para la
supresión de entidad, EF podría no darse cuenta de que hay entidades relacionadas que se
eliminarán.

Actualizar la vista Eliminar

En Views/Student/Delete.cshtml , agregue un mensaje de error entre el encabezado h2 y el


encabezado h3, como se muestra en el siguiente ejemplo:

<h2>Delete</h2>
<p class="text-danger">@ViewData["ErrorMessage"]</p>
<h3>Are you sure you want to delete this?</h3>

Ejecute la página seleccionando la ficha Students y haciendo clic en


un hipervínculo Delete :
Haga clic en Delete . La página Index se muestra sin el estudiante eliminado. (Verá un
ejemplo del código de manejo de errores en acción en el tutorial de concurrencia.)

Cierre de conexiones de base de datos

Para liberar los recursos que tiene una conexión de base de datos, la instancia de contexto
debe eliminarse lo antes posible cuando haya terminado con
ella. La inyección de dependencia de incorporada de ASP.NET Core se encarga de esa tarea
para usted.

En Startup.cs , llama al método de extensión AddDbContext para proporcionar la


clase DbContext en el contenedor DI de ASP.NET. Este método establece la vida útil del
servicio Scoped de forma predeterminada. Scoped significa que la vida útil del objeto de
contexto coincide con el tiempo de vida de solicitud de la página web, y el método Dispose
se llamará automáticamente al final de la solicitud de la web.
Manejo de transacciones

De forma predeterminada, Entity Framework implementa implícitamente transacciones. En


los escenarios en los que realiza cambios en varias filas o tablas y, a continuación se
llama SaveChanges . Entity Framework se asegura automáticamente de que todos los
cambios tengan éxito o todos fallen. Si se realizan algunos cambios primero y luego ocurre
un error, esos cambios se revierten automáticamente.

Consultas sin seguimiento

Cuando un contexto de base de datos recupera filas de tabla y crea objetos de entidad que
los representan, de forma predeterminada mantiene un seguimiento de si las entidades en
memoria están sincronizadas con lo que hay en la base de datos. Los datos en memoria
actúan como caché y se utilizan cuando se actualiza una entidad. Este almacenamiento en
caché es a menudo innecesario en una aplicación web porque las instancias de contexto
son típicamente de corta duración (se crea una nueva y se elimina para cada solicitud) y el
contexto que lee una entidad se suele eliminar antes de que esa entidad se utilice de nuevo.

Puede desactivar el seguimiento de objetos de entidad en la memoria llamando al


método AsNoTracking . Los escenarios típicos en los que puede que desee hacer que
incluyen lo siguiente:

• Durante la vida del contexto, no es necesario actualizar ninguna entidad y no es


necesario que EF cargue automáticamente las propiedades de navegación con
entidades recuperadas mediante consultas independientes . Con frecuencia, estas
condiciones se cumplen en los métodos de acción HttpGet de un controlador.
• Está ejecutando una consulta que recupera un gran volumen de datos y sólo se
actualiza una pequeña parte de los datos devueltos. Puede ser más eficiente desactivar
el seguimiento de la consulta grande y ejecutar una consulta más tarde para las
entidades que necesitan actualizarse.
• Desea adjuntar una entidad para actualizarla, pero antes recuperó la misma entidad
con un propósito diferente. Dado que la entidad ya está siendo rastreada por el
contexto de la base de datos, no puede adjuntar la entidad que desea cambiar. Una
manera de manejar esta situación es llamar AsNoTracking a la consulta anterior.

Resumen

Ahora tiene un conjunto completo de páginas que realizan operaciones CRUD simples para
entidades Estudiantes. En el siguiente tutorial, expandirá la funcionalidad de la página
Index agregando clasificación, filtrado y paginación.
Clasificación, filtrado, paginación y agrupación -
EF Core con ASP.NET Core MVC tutorial (3 of 10)
Objetivo: Agregará funcionalidad de clasificación, filtrado y paginación a la página
Índice de estudiantes.

La siguiente ilustración muestra cómo será la página cuando termine. Los encabezados de
columna son vínculos sobre los que el usuario puede hacer clic para ordenar por esa
columna. Al hacer clic en un encabezado de columna se alterna repetidamente entre orden
ascendente y descendente.
Agregar enlaces de clasificación de columnas a la página de índice de
estudiantes

Para agregar la clasificación a la página Index del estudiante, cambie el método Index del
controlador Estudiantes y agregue también el siguiente código a la vista Índice del
estudiante.

Agregue funcionalidad de clasificación al método Index

En StudentsController.cs , reemplace el método Index con el código siguiente:

public async Task<IActionResult> Index(string sortOrder)


{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
var students = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}

Este código recibe un parámetro sortOrder de la cadena de consulta en la URL. El valor de


cadena de consulta es proporcionado por ASP.NET Core MVC como un parámetro al
método de acción. El parámetro será una cadena que es "Nombre" o "Fecha", seguido
opcionalmente de un subrayado y la cadena "desc" para especificar el orden
descendente. El orden de clasificación predeterminado es ascendente.

La primera vez que se solicita la página Index, no hay ninguna cadena de consulta. Los
estudiantes se muestran en orden ascendente por apellido, que es el valor predeterminado
establecido por el caso por defecto en la declaración switch . Cuando el usuario hace clic en
un hipervínculo de encabezado de columna, a sortOrder se le proporciona el
valor apropiado en la cadena de consulta.

La vista ViewData utiliza los dos elementos (NameSortParm y DateSortParm) para configurar
los hipervínculos de encabezado de columna con los valores de cadena de consulta
apropiados.

public async Task<IActionResult> Index(string sortOrder)


{

ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";

ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";


var students = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}

Estas son declaraciones ternarias. El primero especifica que si el parámetro sortOrder es


nulo o vacío, NameSortParm se debe establecer en "name_desc"; de lo contrario, debe
establecerse en una cadena vacía. Estas dos instrucciones permiten a la vista establecer los
hipervínculos de encabezado de columna de la siguiente manera:
Orden actual Hipervínculo del apellido Hipervínculo de fecha

Apellido ascendente descendente ascendiendo

Apellido descendente ascendiendo ascendiendo

Fecha ascendente ascendiendo descendente

Fecha descendente ascendiendo ascendiendo

El método utiliza LINQ to Entities para especificar la columna por donde ordenar. El código
crea una variable IQueryable antes de la instrucción switch, la modifica en la instrucción
switch y llama al método ToListAsync después de la instrucción switch . Al crear y
modificar variables IQueryable , no se envía ninguna consulta a la base de datos. La consulta
no se ejecuta hasta convertir el objeto IQueryable en una colección llamando a un método
como ToListAsync . Por lo tanto, este código da como resultado una consulta única que no
se ejecuta hasta la instrucción return View .

Agregue hipervínculos de encabezado de columna a la vista Índice de alumnos

Reemplace el código en Views/Students/Index.cshtml , con el código siguiente para agregar


hipervínculos de encabezado de columna. Las líneas cambiadas se resaltan.

@model IEnumerable<ContosoUniversity.Models.Student>

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-action="Index" asp-route-
sortOrder="@ViewData["NameSortParm"]">@Html.DisplayNameFor(model => model.LastName)</a>
</th>
<th>
@Html.DisplayNameFor(model => model.FirstMidName)
</th>
<th>
<a asp-action="Index" asp-route-
sortOrder="@ViewData["DateSortParm"]">@Html.DisplayNameFor(model =>
model.EnrollmentDate)</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Este código utiliza la información en las propiedades ViewData para establecer


hipervínculos con los valores de cadena de consulta apropiados.

Ejecute la aplicación, seleccione la pestañas Students y haga clic en los encabezados


de Last Name y de Enrollment Date para verificar que funciona la clasificación.
Agregar un cuadro de búsqueda a la página Index de estudiantes

Para agregar filtrado a la página de índice de estudiantes, se agregará un cuadro de texto y


un botón a la vista y realizando los cambios correspondientes en el método Index . El
cuadro de texto le permitirá introducir una cadena para buscar en los campos de nombre y
apellido.

Agregar funcionalidad de filtrado al método Index

En StudentsController.cs , reemplace el método Index con el código siguiente (los cambios


se resaltan).

public async Task<IActionResult> Index(string sortOrder, string searchString)

{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
ViewData["CurrentFilter"] = searchString;

var students = from s in _context.Students


select s;

if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));

}
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}

Ha agregado un parámetro searchString al método Index . El valor de la cadena de


búsqueda se recibe desde un cuadro de texto que agregará a la vista Index. También ha
agregado a la sentencia LINQ una cláusula where que selecciona sólo a los estudiantes cuyo
nombre o apellido contiene la cadena de búsqueda. La sentencia que agrega la cláusula
where se ejecuta sólo si hay un valor para buscar.

Nota

Aquí se está llamando el método Where obre un IQueryable objeto, y el filtro se procesará
en el servidor. En algunos escenarios puede estar llamando al método Where como un
método de extensión en una colección en memoria. (Por ejemplo, supongamos que cambia
la referencia para _context.Students que en lugar de una EF DbSet haga referencia a un
método de repositorio que devuelva una colección IEnumerable ). El resultado sería
normalmente el mismo, pero en algunos casos puede ser diferente.
Por ejemplo, la implementación de .NET Framework del método Contains realiza una
comparación sensible a mayúsculas y minúsculas por defecto, pero en SQL Server esto está
determinado por la configuración de intercalación de la instancia de SQL Server. Ese ajuste
predeterminado es insensible a mayúsculas y minúsculas. Puede llamar al método ToUpper
para que la prueba sea explícitamente distinta de mayúsculas y minúsculas: Where (s =>
s.LastName.ToUpper (). Contiene (searchString.ToUpper ()) . Esto aseguraría que los
resultados permanezcan iguales si cambia el código más tarde para usar un repositorio que
devuelve una colección IEnumerable en lugar de un objeto IQueryable (cuando llama al
método Contains en una colección IEnumerable , obtiene la implementación de .NET
Framework; cuando se llama sobre un objeto IQueryable , obtendrá la implementación de
proveedor de base de datos.) Sin embargo, hay una penalización de rendimiento para esta
solución. El código ToUpper pondría una función en la cláusula WHERE de la instrucción
SELECT de TSQL. Esto impediría que el optimizador utilizara un índice. Teniendo en cuenta
que SQL se instala por defecto sin distinción de mayúsculas y minúsculas, es mejor evitar el
código ToUpper hasta que migre a un almacén de datos sensible a mayúsculas y
minúsculas.

Agregar un cuadro de búsqueda a la vista del índice del estudiante

En Views/Student/Index.cshtml , agregue el código resaltado inmediatamente antes de la


etiqueta de apertura de la etiqueta de tabla para crear un subtítulo, un cuadro de texto y
un botón de búsqueda Search .

<p>
<a asp-action="Create">Create New</a>
</p>

<form asp-action="Index" method="get">


<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString"
value="@ViewData["currentFilter"]" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-action="Index">Back to Full List</a>
</p>
</div>
</form>

<table class="table">
Este código utiliza el ayudante de etiquetas <form> para agregar el cuadro de texto de
búsqueda y el botón. De forma predeterminada, el ayudante de etiquetas <form> envía
datos de formulario con un POST, lo que significa que los parámetros se pasan en el cuerpo
del mensaje HTTP y no en la URL como cadenas de consulta. Cuando especifica HTTP GET,
los datos del formulario se pasan en la URL como cadenas de consulta, lo que permite a los
usuarios marcar la URL. Las directrices del W3C recomiendan que utilice GET cuando la
acción no da lugar a una actualización.

Ejecute la página, ingrese una cadena de búsqueda y haga clic en el botón Search para
verificar que el filtrado está funcionando.

Observe que la URL contiene la cadena de búsqueda.

http://localhost:5813/Students?SearchString=an

Si marca esta página, obtendrá la lista filtrada cuando utilice el marcador. La


adición method="get" a la etiqueta form es la causa de la generación de la cadena de
consulta.
En este momento, si hace clic en un enlace de clasificación de encabezado de columna,
perderá el valor del texto de filtro que ingresó en el cuadro Buscar . Lo solucionaremos en
la siguiente sección.

Agregar funcionalidad de paginación a la página Índice de estudiantes

Para agregar paginación a la página de índice de estudiantes, creará una


clase PaginatedList que utiliza las declaraciones Skip y Take para filtrar datos en el
servidor en lugar de siempre recuperar todas las filas de la tabla. A continuación, realizará
cambios adicionales en el método Index y agregará botones de paginación a la vista
Index . La siguiente ilustración muestra los botones de paginación.

En la carpeta de proyecto, cree PaginatedList.cs y, a continuación, reemplace el código de


plantilla con el código siguiente.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }

public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)


{
PageIndex = pageIndex;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);

this.AddRange(items);
}

public bool HasPreviousPage


{
get
{
return (PageIndex > 1);
}
}

public bool HasNextPage


{
get
{
return (PageIndex < TotalPages);
}
}

public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int


pageIndex, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip((pageIndex - 1) *
pageSize).Take(pageSize).ToListAsync();
return new PaginatedList<T>(items, count, pageIndex, pageSize);
}
}
}
El método CreateAsync en este código toma el tamaño de página y el número de página y
se aplica las adecuadas declaraciones Skip y Take al IQueryable . Cuando se llama
a ToListAsync, IQueryable devolverá una lista que contenga sólo la página solicitada. Las
propiedades HasPreviousPage y HasNextPage se pueden utilizar para activar o desactivar los
botones de paginación Previous y Next .

Se utiliza un método CreateAsync en lugar de un constructor para crear el


objeto PaginatedList<T> porque los constructores no pueden ejecutar código asíncrono.

Agregar funcionalidad de paginación al método Index

En StudentsController.cs , reemplace el método Index con el código siguiente.

public async Task<IActionResult> Index(


string sortOrder,
string currentFilter,
string searchString,
int? page)

{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";

if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;

ViewData["CurrentFilter"] = searchString;

var students = from s in _context.Students


select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}

int pageSize = 3;

return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page


?? 1, pageSize));
}

Este código agrega un parámetro de número de página, un parámetro de orden de


clasificación actual y un parámetro de filtro actual a la firma de método.

public async Task<IActionResult> Index(


string sortOrder,
string currentFilter,
string searchString,
int? page)

La primera vez que se muestre la página, o si el usuario no ha hecho clic en un enlace de


paginación o clasificación, todos los parámetros serán nulos. Si se hace clic en un enlace de
paginación, la variable de página contendrá el número de página que se mostrará.

El elemento ViewData denominado CurrentSort proporciona la vista con el orden actual, ya


que debe incluirse en los enlaces de paginación para mantener el orden de clasificación
durante la paginación.

El elemento ViewData denominado CurrentFilter proporciona la vista con la cadena de filtro


actual. Este valor debe incluirse en los enlaces de paginación para mantener la
configuración del filtro durante la paginación y debe restaurarse en el cuadro de texto
cuando se vuelva a mostrar la página.

Si la cadena de búsqueda se cambia durante la paginación, la página debe restablecerse a


1, ya que el nuevo filtro puede resultar en datos diferentes para mostrar. La cadena de
búsqueda se cambia cuando se introduce un valor en el cuadro de texto y se presiona el
botón Enviar. En ese caso, el parámetro searchString no es nulo.

if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}

Al final del método Index , el método PaginatedList.CreateAsync convierte la consulta del


estudiante en una sola página de alumnos en un tipo de colección que admita la
paginación. Esa única página de estudiantes se pasa a la vista.

return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1,


pageSize));

El método PaginatedList.CreateAsync toma un número de página. Los dos signos de


interrogación representan el operador de coagulación nula. El operador de coagulación
nula define un valor predeterminado para un tipo anulable; la expresión (page ??
1) significa devolver el valor de page si tiene un valor, o devolver 1 si page es nulo.

Agregar vínculos de paginación a la vista Índice de alumnos

En Views/Students/Index.cshtml , reemplace el código existente con el código siguiente. Los


cambios están resaltados.

@model PaginatedList<ContosoUniversity.Models.Student>

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>

<form asp-action="Index" method="get">


<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString"
value="@ViewData["currentFilter"]" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-action="Index">Back to Full List</a>
</p>
</div>
</form>

<table class="table">
<thead>
<tr>
<th>
<a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]"
asp-route-currentFilter="@ViewData["CurrentFilter"]">Last Name</a>
</th>
<th>
First Name
</th>
<th>
<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]"
asp-route-currentFilter="@ViewData["CurrentFilter"]">Enrollment Date</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

@{
var prevDisabled = !Model.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.HasNextPage ? "disabled" : "";
}

<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-page="@(Model.PageIndex - 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-page="@(Model.PageIndex + 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @nextDisabled">
Next
</a>

La instrucción @model en la parte superior de la página especifica que la vista ahora obtiene
un objeto PaginatedList<T> en lugar de un objeto List<T> .

Los enlaces de encabezado de columna utilizan la cadena de consulta para pasar la cadena
de búsqueda actual al controlador para que el usuario pueda ordenar dentro de los
resultados del filtro:

<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-


currentFilter ="@ViewData["CurrentFilter"]">Enrollment Date</a>

Los botones de paginación son mostrados por ayudantes de etiquetas:

<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-page="@(Model.PageIndex - 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @prevDisabled">
Previous
</a>

Ejecutar la página.

Haga clic en los enlaces de paginación en diferentes ordenaciones para asegurarse de que
funciona la paginación. A continuación, introduzca una cadena de búsqueda e intente
buscar otra vez para verificar que la paginación también funciona correctamente con la
clasificación y el filtrado.

Crear una página About en la que se muestran Estadísticas de los alumnos

La página About de la Universidad Contoso , mostrará cuántos estudiantes se han inscrito


para cada fecha de inscripción. Esto requiere la agrupación y cálculos simples en los
grupos. Para lograr esto, usted hará lo siguiente:
• Cree una clase de modelo de vista para los datos que necesita pasar a la vista.
• Modifique el método About en el controlador Home.
• Modifique la vista About.

Crear el modelo de vista

Cree una carpeta SchoolViewModels en la carpeta Models.

En la nueva carpeta, agregue un archivo de clase EnrollmentDateGroup.cs y reemplace el


código de plantilla con el código siguiente:

using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }

public int StudentCount { get; set; }


}
}

Modificar el controlador de la página Home

En HomeController.cs , agregue las siguientes instrucciones de uso en la parte superior del


archivo:

using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;

Agregue una variable de clase para el contexto de la base de datos inmediatamente


después de la abrazadera de apertura para la clase y obtenga una instancia del contexto de
DI de ASP.NET Core :

public class HomeController : Controller


{
private readonly SchoolContext _context;
public HomeController(SchoolContext context)
{
_context = context;
}

Reemplace el método About con el código siguiente:

public async Task<ActionResult> About()


{
IQueryable<EnrollmentDateGroup> data =
from student in _context.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
return View(await data.AsNoTracking().ToListAsync());
}

La instrucción LINQ agrupa las entidades Students por fecha de inscripción, calcula el
número de entidades de cada grupo y almacena los resultados en una colección de
objetos EnrollmentDateGroup de modelo de vista.

Modificar la vista About

Reemplace el código en el archivo Views / Home / About.cshtml con el código siguiente:

@model IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>

@{
ViewData["Title"] = "Student Body Statistics";
}

<h2>Student Body Statistics</h2>

<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>

@foreach (var item in Model)


{
<tr>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>

Ejecute la aplicación y haga clic en el enlace About . El número de estudiantes para cada
fecha de inscripción se muestra en una tabla.
Migraciones - EF Core con ASP.NET Core MVC
tutorial (4 of 10)
Objetivo: Utilizar la función de migraciones EF Core para gestionar los cambios del
modelo de datos.

Introducción a las migraciones

Al desarrollar una nueva aplicación, el modelo de datos cambia con frecuencia y cada vez
que cambia el modelo, se descompone con la base de datos. Comenzó configurando Entity
Framework para crear la base de datos si no existe. A continuación, cada vez que cambie el
modelo de datos, agregue, elimine o cambie las clases de entidad o cambie su clase
DbContext, puede eliminar la base de datos y EF crea una nueva que coincide con el
modelo y la semilla con datos de prueba.

Este método de mantener la base de datos en sincronía con el modelo de datos funciona
bien hasta que despliegue la aplicación en producción. Cuando la aplicación se ejecuta en
producción, suele almacenar datos que desea conservar y no desea perder todo cada vez
que realiza un cambio, como añadir una nueva columna. La característica EF Core
Migrations soluciona este problema al permitir que EF actualice el esquema de la base de
datos en lugar de crear una nueva base de datos.

Entity Framework Core NuGet Paquetes para migraciones

Para trabajar con migraciones, puede utilizar la Consola del gestor de paquetes (PMC) o
la interfaz de línea de comandos (CLI). Estos tutoriales muestran cómo usar los comandos
CLI. La información sobre el PMC está al final de este tutorial .

Las herramientas EF para la interfaz de línea de comandos (CLI) se proporcionan


en Microsoft.EntityFrameworkCore.Tools.DotNet . Para instalar este paquete, agréguelo a la
colección DotNetCliToolReference en el archivo .csproj , como se
muestra. Nota: Debe instalar este paquete editando el archivo .csproj ; no puede utilizar el
comando install-package o la GUI del gestor de paquetes. Puede editar
el archivo .csproj haciendo clic con el botón secundario en el nombre del proyecto en
el Explorador de soluciones y seleccionando Editar ContosoUniversity.csproj .

<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet"
Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools"
Version="2.0.0" />
</ItemGroup>

(Los números de versión en este ejemplo eran actuales cuando se escribió el tutorial.)

Cambiar la cadena de conexión

En el archivo appsettings.json , cambie el nombre de la base de datos en la cadena de


conexión a ContosoUniversity2 o algún otro nombre que no haya utilizado en el equipo que
está utilizando.

{
"ConnectionStrings": {
"DefaultConnection":
"Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity2;Trusted_Connection=True;Mult
ipleActiveResultSets=true"
},

Este cambio configura el proyecto para que la primera migración cree una nueva base de
datos. Esto no es necesario para comenzar con las migraciones, pero verá más adelante por
qué es una buena idea.

Nota

Como alternativa al cambio del nombre de la base de datos, puede eliminar la base de
datos. Utilice el Explorador de objetos de SQL Server (SSOX) o el comando CLI database
drop :

dotnet ef database drop

La siguiente sección explica cómo ejecutar comandos CLI.

Crear una migración inicial

Guarde los cambios y cree el proyecto. A continuación, abra una ventana de comandos y
navegue hasta la carpeta del proyecto. La manera rápida de hacer eso:

• En el Explorador de soluciones , haga clic con el botón secundario en el proyecto y


elija Abrir en el Explorador de archivos en el menú contextual.
• Introduzca "cmd" en la barra de direcciones y pulse Intro.

Escriba el siguiente comando en la ventana de comandos:

dotnet ef migrations add InitialCreate

Verá una salida como la siguiente en la ventana de comandos:

info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using
'C:\Users\username\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and
Windows DPAPI to encrypt keys at rest.
info: Microsoft.EntityFrameworkCore.Infrastructure[100403]
Entity Framework Core 2.0.0-rtm-26452 initialized 'SchoolContext' using provider
'Microsoft.EntityFrameworkCore.SqlServer' with options: None
Done. To undo this action, use 'ef migrations remove'

Nota

Si aparece un mensaje de error, no se encuentra ningún ejecutable que coincida con el


comando "dotnet-ef" , consulte esta publicación del blog (http://thedatafarm.com/data-
access/no-executable-found-matching-command-dotnet-ef/) para obtener ayuda sobre la
solución de problemas. Posiblemente tenga que instalar el siguiente paquete NuGet, salir
del Visual Studio y volver a cargar el proyecto.

Compruebe el contenido del archivo ContosoUniversity.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design"
Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet"
Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools"
Version="2.0.0" />
</ItemGroup>
</Project>

Si aparece un mensaje de error " no puede acceder al archivo ... ContosoUniversity.dll porque
está siendo utilizado por otro proceso. ", Busque el ícono de IIS Express en la bandeja del
sistema de Windows, haga clic con el botón derecho en él y, a continuación, haga clic
en ContosoUniversity> Stop Site .
Examinar los métodos Up y Down

Cuando ejecutó el comando migrations add , EF generó el código que creará la base de
datos desde cero. Este código se encuentra en la carpeta Migrations , en el archivo
denominado <timestamp> _InitialCreate.cs . El método Up de la clase InitialCreate crea
las tablas de base de datos que corresponden a los conjuntos de entidades del modelo de
datos y el método Down los elimina, como se muestra en el siguiente ejemplo.

public partial class InitialCreate : Migration


{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Course",
columns: table => new
{
CourseID = table.Column<int>(nullable: false),
Credits = table.Column<int>(nullable: false),
Title = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Course", x => x.CourseID);
});

// Additional code not shown


}

protected override void Down(MigrationBuilder migrationBuilder)


{
migrationBuilder.DropTable(
name: "Enrollment");
// Additional code not shown
}
}

Migrations llama al método Up para implementar los cambios del modelo de datos para
una migración. Para revertir la actualización, Migrations llama al método Down .

Este código es para la migración inicial que se creó al ingresar el comando migrations add
InitialCreate . El nombre del archivo de la migración es un parámetro ("InitialCreate" en el
ejemplo) y puede ser lo que quieras. Lo mejor es elegir una palabra o frase que resume lo
que se está haciendo en la migración. Por ejemplo, podría nombrar una migración posterior
"AddDepartmentTable".
Si creó la migración inicial cuando ya existe la base de datos, se genera el código de
creación de la base de datos pero no tiene que ejecutarse porque la base de datos ya
coincide con el modelo de datos. Cuando implemente la aplicación en otro entorno donde
la base de datos aún no existe, este código se ejecutará para crear su base de datos, por lo
que es una buena idea probarlo primero. Es por eso que cambió el nombre de la base de
datos en la cadena de conexión anterior, para que las migraciones puedan crear una nueva
desde cero.

Examinar la instantánea del modelo de datos

Migrations también crea una instantánea del esquema de base de datos actual
en Migrations/SchoolContextModelSnapshot.cs . A continuación se muestra el aspecto del
código:

[DbContext(typeof(SchoolContext))]
partial class SchoolContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452")
.HasAnnotation("SqlServer:ValueGenerationStrategy",
SqlServerValueGenerationStrategy.IdentityColumn);

modelBuilder.Entity("ContosoUniversity.Models.Course", b =>
{
b.Property<int>("CourseID");

b.Property<int>("Credits");

b.Property<string>("Title");

b.HasKey("CourseID");

b.ToTable("Course");
});

// Additional code for Enrollment and Student tables not shown

modelBuilder.Entity("ContosoUniversity.Models.Enrollment", b =>
{
b.HasOne("ContosoUniversity.Models.Course", "Course")
.WithMany("Enrollments")
.HasForeignKey("CourseID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ContosoUniversity.Models.Student", "Student")
.WithMany("Enrollments")
.HasForeignKey("StudentID")
.OnDelete(DeleteBehavior.Cascade);
});
}
}

Debido a que el esquema de la base de datos actual está representado en el código, EF


Core no tiene que interactuar con la base de datos para crear migraciones. Cuando agrega
una migración, EF determina qué cambió comparando el modelo de datos con el archivo de
instantánea. EF interactúa con la base de datos sólo cuando tiene que actualizar la base de
datos.

El archivo de instantánea debe mantenerse sincronizado con las migraciones que lo crean,
por lo que no puede eliminar una migración simplemente eliminando el archivo
denominado <timestamp> _ <nombre_migración> .cs . Si elimina ese archivo, las
migraciones restantes no estarán sincronizadas con el archivo de instantánea de la base de
datos. Para eliminar la última migración que ha agregado, utilice el comando dotnet ef
migrationtions remove .

Aplicar la migración a la base de datos

En la ventana de comandos, ingrese el siguiente comando para crear la base de datos y las
tablas en ella.

dotnet ef database update

La salida del comando es similar al comando migrations add , excepto que ve los registros
de los comandos SQL que configuran la base de datos. La mayoría de los registros se omite
en la salida del ejemplo siguiente. Si prefiere no ver este nivel de detalle en los mensajes de
registro, puede cambiar los niveles de registro en el archivo appsettings.Development.json .

info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using
'C:\Users\username\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and
Windows DPAPI to encrypt keys at rest.
info: Microsoft.EntityFrameworkCore.Infrastructure[100403]
Entity Framework Core 2.0.0-rtm-26452 initialized 'SchoolContext' using provider
'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[200101]
Executed DbCommand (467ms) [Parameters=[], CommandType='Text',
CommandTimeout='60']
CREATE DATABASE [ContosoUniversity2];
info: Microsoft.EntityFrameworkCore.Database.Command[200101]
Executed DbCommand (20ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE [__EFMigrationsHistory] (
[MigrationId] nvarchar(150) NOT NULL,
[ProductVersion] nvarchar(32) NOT NULL,
CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
);

<logs omitted for brevity>

info: Microsoft.EntityFrameworkCore.Database.Command[200101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20170816151242_InitialCreate', N'2.0.0-rtm-26452');
Done.

Utilice el Explorador de objetos de SQL Server para inspeccionar la base de datos como
lo hizo en el primer tutorial. Notará la adición de una tabla de __EFMigrationsHistory que
realiza un seguimiento de las migraciones que se han aplicado a la base de datos. Vea los
datos de esa tabla y verá una fila para la primera migración. (El último registro del ejemplo
de salida de la CLI anterior muestra la sentencia INSERT que crea esta fila.)

Ejecute la aplicación para comprobar que todo sigue funcionando igual que antes.
Interfaz de línea de comandos (CLI) vs. Consola del gestor de paquetes (PMC)

La herramienta EF para gestionar las migraciones está disponible en los comandos de la CLI
de .NET Core o en los cmdlets de PowerShell en la ventana de la Consola del gestor de
paquetes de Visual Studio (PMC). Este tutorial muestra cómo usar la CLI, pero puede usar
el PMC si lo prefiere.

Los comandos EF para los comandos PMC se encuentran en


el paquete Microsoft.EntityFrameworkCore.Tools . Este paquete ya está incluido en
el metapackage Microsoft.AspNetCore.All , por lo que no tiene que instalarlo.

Importante: Este no es el mismo paquete que el que se instala para la CLI al editar
el archivo .csproj . El nombre de éste termina Tools , a diferencia del nombre del paquete
CLI que termina en Tools.DotNet .
Creación de un modelo de datos complejos - EF
Core con ASP.NET Core MVC tutorial (5 of 10)
Objetivo: agregar más entidades y relaciones y personalizará el modelo de datos
especificando las reglas de asignación de formato, validación y base de datos.

Cuando haya terminado, las clases de entidad formarán el modelo de datos que se muestra
en la siguiente ilustración:
Personalizar el modelo de datos mediante los atributos

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

El atributo DataType

Para las fechas de inscripción de estudiantes, todas las páginas web muestran actualmente
el tiempo junto con la fecha, aunque lo único que te importa para este campo es la
fecha. Mediante el uso de los atributos de anotación de datos, se puede realizar un sólo
cambio de código que modifique el formato de visualización en cada una de las vistas que
muestre los datos. Para ver un ejemplo de cómo hacerlo, agregará un atributo a la
propiedad EnrollmentDate de la clase Student .

En Models/Student.cs , agregue una insrucción using para el espacio de


nombres System.ComponentModel.DataAnnotations y agregue 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 ICollection<Enrollment> Enrollments { get; set; }


}
}
El atributo DataType se utiliza para especificar un tipo de datos que es más específico que el
tipo intrínseco de base de datos. En este caso sólo queremos llevar un registro de la fecha,
no de la fecha y la hora. La Enumeración DataType proporciona muchos tipos de datos,
como Fecha, Hora, Número de teléfono, Moneda, EmailAddress, etc…. El atributo DataType
también puede permitir que la aplicación proporcione automáticamente características
específicas de tipo. Por ejemplo, el enlace mailto: se puede
crear para DataType.EmailAddress , y un selector de fecha puede ser proporcionado
por DataType.Date en los navegadores compatibles con HTML5. El atributo DataType
genera atributos de HTML 5 data- (pronunciado data dash) que los navegadores HTML 5
pueden entender. Los atributos DataType no proporcionan ninguna validación.

DataType.Date no especifica el formato de la fecha que se muestra. De forma


predeterminada, el campo de datos se muestra de acuerdo con los formatos
predeterminados basados en el CultureInfo del servidor.

El atributo DisplayFormat se utiliza para especificar explícitamente el formato de fecha:

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

La configuración ApplyFormatInEditMode especifica que el formato también debe aplicarse


cuando el valor se muestra en un cuadro de texto para su edición. (Es posible que no quiera
que para algunos campos, por ejemplo, para los valores de moneda, no quiera que el
símbolo de moneda esté en el cuadro de texto para su edición).

Puede utilizar el atributo DisplayFormat por sí mismo, pero generalmente es una buena
idea usar el atributo DataType también. El atributo DataType conlleva la semántica de los
datos en lugar de cómo hacerlo en una pantalla, y proporciona los siguientes beneficios
que no se obtienen con DisplayFormat :

• El navegador puede habilitar las funciones de HTML5 (por ejemplo, mostrar un control
de calendario, el símbolo de moneda apropiado para la configuración regional, los
enlaces de correo electrónico, una validación de entrada del cliente, etc.).
• De forma predeterminada, el navegador procesará los datos utilizando el formato
correcto en función de su entorno.

Vuelva a ejecutar la página de índice de estudiantes y observe que ya no se muestra el


tiempo en las fechas de inscripción. Lo mismo será cierto para cualquier vista que utilice el
modelo de Student.
El atributo StringLength

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 del campo en la
base de datos y proporciona validación en el lado del cliente y en el lado del servidor para
ASP.NET MVC. También puede especificar la longitud mínima de cadena en este atributo,
pero el valor mínimo no tiene impacto en el esquema de la base de datos.

Supongamos que desea asegurarse de que los usuarios no introduzcan más de 50


caracteres para un nombre. Para agregar esta limitación, añada 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 ICollection<Enrollment> Enrollments { get; set; }


}
}

El atributo StringLength no impedirá que un usuario introduzca un espacio en blanco para


un nombre. Puede utilizar el atributo RegularExpression para aplicar restricciones a la
entrada. Por ejemplo, el siguiente código requiere que el primer carácter sea mayúscula y
que los caracteres restantes sean alfabéticos:

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

El atributo MaxLength proporciona una funcionalidad similar al atributo StringLength pero


no proporciona validación del lado del cliente.

El modelo de base de datos ha cambiado de una manera que requiere un cambio en el


esquema de la base de datos. Utilizará las migraciones para actualizar el esquema sin
perder ningún dato que pueda haber agregado a la base de datos mediante la interfaz de
usuario de la aplicación.

• Guarde los cambios y cree el proyecto. A continuación, abra la ventana de comandos


en la carpeta del proyecto (En el Explorador de soluciones , haga clic con el botón
secundario en el proyecto y elija Abrir en el Explorador de archivos en el menú
contextual y escriba cmd en la barra de archivos) e introduzca los siguientes
comandos:

dotnet ef migrations add MaxLengthOnNames


dotnet ef database update

El comando migrations add advierte que la pérdida de datos puede ocurrir, porque el
cambio hace que la longitud máxima sea más corta para dos columnas. Migrations 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 database update ejecutó ese código.

Entity Framework utiliza la marca de tiempo prefijada al nombre de archivo de migraciones


para ordenar las migraciones. Puede crear varias migraciones antes de ejecutar el comando
update-database y, a continuación, todas las migraciones se aplicarán en el orden en que se
crearon.

Ejecute la página Create e introduzca un nombre de más de 50 caracteres. Al hacer clic en


el botón Create, la validación del lado del cliente muestra un mensaje de error.

3
El atributo Column

También puede utilizar atributos para controlar cómo se correlacionan sus clases y
propiedades con la base de datos. Supongamos que ha utilizado el nombre FirstMidName
para el campo del primer nombre porque el campo también podría contener un segundo
nombre. Pero desea que se nombre la columna de la base de datos FirstName , ya que los
usuarios que estarán escribiendo consultas ad-hoc en la base de datos están
acostumbrados a ese nombre. Para realizar esta asignación, puede utilizar el
atributo Column .

El atributo Column especifica que cuando se crea la base de datos, se nombrará la columna
de la table Student que se asigna a la propiedad FirstMidName se llamará FirstName . En
otras palabras, cuando su código se refiera a Student.FirstMidName , los datos provendrá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 nombre de la propiedad.

En el archivo Student.cs , agregue una sentencia using


para System.ComponentModel.DataAnnotations.Schema y agregue el atributo de nombre de
columna a la propiedad FirstMidName , como se muestra en 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; }


}
}
La adición del atributo Column cambia el modelo que respalda el SchoolContext , por lo que
no coincidirá con la base de datos.

Guarde los cambios y compile el proyecto. A continuación, abra la ventana de comandos en


la carpeta del proyecto e introduzca los siguientes comandos para crear otra migración:

dotnet ef migrations add ColumnFirstName


dotnet ef database update

En el Explorador de objetos de SQL Server , abra el Diseñador de tablas de alumnos


haciendo doble clic en la tabla Students .

Antes de aplicar las dos primeras migraciones, las columnas de nombre eran de tipo
nvarchar (MAX). Ahora son nvarchar (50) y el nombre de la columna ha cambiado de
FirstMidName a FirstName.

Nota

Si intenta compilar antes de terminar de crear todas las clases de entidad en las siguientes
secciones, es posible que obtenga errores del compilador.
Cambios finales a la entidad Estudiante

En Models/Student.cs , reemplace el código que agregó anteriormente con el código


siguiente. Los cambios están 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 ICollection<Enrollment> Enrollments { get; set; }


}
}

El atributo Requerido

El atributo Required hace que los campos de propiedades de nombre sean obligatorios. El
atributo Required no es necesario para tipos no anulables como tipos de valores (DateTime,
int, double, float, etc.). Los tipos que no pueden ser nulos se tratan automáticamente como
campos obligatorios.

Puede eliminar el atributo Required y reemplazarlo por un parámetro de longitud mínima


para el atributo StringLength :

[Display(Name = "Last Name")]


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

El atributo Mostrar

El atributo Display especifica que el título de los cuadros de texto debe ser "Nombre",
"Apellido", "Nombre completo" y "Fecha de inscripción" en lugar del nombre de la
propiedad en cada caso (que no tiene espacio dividiendo las palabras).

La propiedad calculada

FullName es una propiedad calculada que devuelve un valor que se crea al concatenar otras
dos propiedades. Por lo tanto, sólo tiene un accessor get, y FullName no
generará ninguna columna en la base de datos.
Crear la entidad Instructor

Cree Models/Instructor.cs , reemplazando 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 ICollection<CourseAssignment> CourseAssignments { get; set; }


public OfficeAssignment OfficeAssignment { get; set; }
}
}

Observe que varias propiedades son las mismas en las entidades del estudiante y del
instructor.

Puede poner varios atributos en una línea, por lo que también podría escribir los
atributos HireDate de la siguiente manera:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString =


"{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

Las propiedades de navegación CourseAssignments y OfficeAssignment

Las propiedades CourseAssignments y OfficeAssignment son propiedades de navegación.

Un instructor puede enseñar 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 puede contener varias entidades, su tipo debe ser una lista
en la que las entradas se pueden agregar, eliminar y actualizar. Puede
especificar ICollection<T> o un tipo como List<T> o HashSet<T> . Si
especifica ICollection<T> , EF crea una HashSet<T> colección de forma predeterminada.

La razón por la cual estas son entidades CourseAssignment se explica a continuación en la


sección sobre las relaciones de muchos a muchos.

Las reglas empresariales de Contoso University establecen que un instructor sólo puede
tener como máximo una oficina, por lo que la propiedad OfficeAssignment tiene una sola
entidad OfficeAssignment (que puede ser nula si no se asigna ninguna oficina).

public OfficeAssignment OfficeAssignment { get; set; }


Crear la entidad OfficeAssignment

Crear models/ OfficeAssignment.cs con el código siguiente:

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 Clave

Hay una relación de uno a cero o uno entre el instructor y las entidades
OfficeAssignment. Una asignación de oficina sólo existe en relación con el instructor al que
está asignado, y por lo tanto, su clave principal es también su clave externa para la entidad
Instructor. Sin embargo, Entity Framework no puede reconocer automáticamente
InstructorID como la clave principal de esta entidad porque su nombre no sigue la
convención de nomenclatura ID o classnameID. Por lo tanto, el atributo Key se utiliza para
identificarlo como la clave:

[Key]
public int InstructorID { get; set; }
También puede utilizar el atributo Key si la entidad tiene su propia clave principal pero
desea nombrar la propiedad de forma particular.

De forma predeterminada, EF trata la clave como no generada en la base de datos porque


la columna es para una relación de identificación.

La propiedad de navegación del instructor

La entidad Instructor tiene una propiedad OfficeAssignment de


navegación anulable (porque un instructor puede no tener una asignación de oficina) y la
entidad OfficeAssignment tiene una propiedad Instructor de navegación no
anulable (porque una asignación de oficina no puede existir sin un instructor
- InstructorID no es anulable). Cuando una entidad Instructor tiene una entidad
OfficeAssignment relacionada, cada entidad tendrá una referencia a la otra en su propiedad
de navegación.

Podría poner un atributo [Required] en la propiedad de navegación Instructor para


especificar que debe haber un instructor relacionado, pero no tiene que hacerlo porque
la InstructorID clave externa (que también es la clave de esta tabla) no es anulable.

Modificar la Entidad del Curso

En Models/Course.cs , reemplace el código que agregó anteriormente con el código


siguiente. Los cambios están resaltados.

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 de curso tiene una propiedad de clave externa DepartmentID que apunta a la
entidad de departamento relacionada y tiene una propiedad de navegación Department .

Entity Framework no requiere que agregue una propiedad de clave externa a su modelo de
datos cuando tenga una propiedad de navegación para una entidad relacionada. EF crea
automáticamente claves externas en la base de datos dondequiera que se necesiten y
crea propiedades de sombra para ellas. Pero tener la clave externa en el modelo de datos
puede hacer las actualizaciones más simples y más eficientes. Por ejemplo, cuando busca
una entidad de curso para editarla, la entidad Departamento es nula si no la carga, por lo
que al actualizar la entidad del curso, primero tendría que buscar la entidad
Departamento. Cuando la propiedad de clave externa DepartmentID se incluye en el
modelo de datos, no es necesario que obtenga la entidad de departamento antes de
actualizar.

El atributo DatabaseGenerated

El atributo DatabaseGenerated con el parámetro None en la propiedad CourseID especifica


que los valores de clave primaria son proporcionados por el usuario en lugar de generados
por la base de datos.

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

De forma predeterminada, Entity Framework asume que los valores de clave primaria son
generados por la base de datos. Eso es lo que quieres en la mayoría de los escenarios. Sin
embargo, para entidades de curso, utilizará un número de curso especificado por el usuario,
como una serie 1000 para un departamento, una serie 2000 para otro departamento, etc.

El atributo DatabaseGenerated también se puede utilizar para generar valores


predeterminados, como en el caso de las columnas de base de datos utilizadas para
registrar la fecha en la que se creó o actualizó una fila.

Clave externa y propiedades de navegación

Las propiedades de clave externa y las propiedades de navegación en la entidad de curso


reflejan las relaciones siguientes:

Un curso se asigna a un departamento, por lo que hay una clave ajena DepartmentID y una
propiedad de navegación Department por las razones mencionadas anteriormente.

public int DepartmentID { get; set; }


public Department Department { get; set; }

Un curso puede tener cualquier número de estudiantes matriculados 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 enseñado por varios instructores, por lo que la propiedad de
navegación CourseAssignments es una colección (el tipo CourseAssignment se explica más
adelante ):

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


Crear la entidad de departamento

Crear 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 Instructor Administrator { get; set; }


public ICollection<Course> Courses { get; set; }
}
}
El atributo Column

Anteriormente utilizó el atributo Column para cambiar la asignación de nombres de


columnas. En el código de la entidad Departamento, Column se utiliza para cambiar la
asignación de tipo de datos SQL para que la columna se defina utilizando el tipo money de
SQL Server en la base de datos:

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

El mapeo de columnas generalmente no se requiere, porque el Entity Framework elige el


tipo de datos SQL Server apropiado basado en el tipo CLR que define para la propiedad. El
tipo decimal CLR se asigna a un tipo decimal de SQL Server . Pero en este caso usted sabe
que la columna estará almacenando cantidades monetarias, y el tipo de datos money es
más apropiado para eso.

Clave externa y propiedades de navegación

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

Un departamento puede o no tener un administrador, y un administrador es siempre un


instructor. Por lo tanto, la propiedad InstructorID se incluye como la clave externa para la
entidad Instructor y se agrega un signo de interrogación después de la designación de
tipo int para marcar la propiedad como anulable. La propiedad de navegación se
denomina Administrator pero contiene una entidad instructora:

public int? InstructorID { get; set; }


public Instructor Administrator { get; set; }

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

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

Nota

Por convención, Entity Framework permite el borrado en cascada para claves ajenas no
anulables y para relaciones de muchos a muchos. Esto puede resultar en reglas de
eliminación de cascadas circulares, lo que causará una excepción cuando intente agregar
una migración. Por ejemplo, si no definió la propiedad Department.InstructorID como
anulable, EF configuraría una regla de eliminación en cascada para eliminar al instructor al
eliminar el departamento, que no es lo que desea que suceda. Si las reglas de negocio
requerían que la propiedad InstructorID no fuese anulable, tendría que usar la siguiente
declaración API para desactivar la eliminación en cascada en la relación:

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

Modificar la entidad de inscripción

En Models/Enrollment.cs , reemplace el código que agregó anteriormente con el código


siguiente:

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

Clave externa y propiedades de navegación

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

Un registro de inscripción es para un solo curso, por lo que hay una propiedad CourseID de
clave externa 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 solo estudiante, por lo que hay una


propiedad StudentID de clave externa y una Student de navegación Student de
navegación:

public int StudentID { get; set; }


public Student Student { get; set; }

Relaciones de muchos a muchos

Hay una relación muchos-a-muchos entre las entidades del estudiante y del curso, y la
entidad de inscripción funciona como una tabla de unión many-to-many con carga útil en la
base de datos. "Con carga útil" significa que la tabla de inscripción contiene datos
adicionales además de claves externas para las tablas unidas (en este caso, una clave
primaria y una propiedad Grado).

La siguiente ilustración muestra cómo son estas relaciones en un diagrama de entidad. (Este
diagrama se generó utilizando Entity Framework Power Tools para EF 6.x, la creación del
diagrama no forma parte del tutorial, solo se utiliza aquí como ilustración).
Cada línea de relación tiene un 1 en un extremo y un asterisco (*) en el otro, indicando una
relación uno-a-muchos.

Si la tabla de inscripción no incluía información de grado, sólo tendría que contener las dos
claves foráneas CourseID y StudentID. En ese caso, sería una tabla de unión many-to-many
sin carga útil (o una tabla de unión pura) en la base de datos. Las entidades Instructor y
Course tienen ese tipo de relación many-to-many, y el siguiente paso es crear una clase de
entidad para que funcione como una tabla de unión sin carga útil.

(EF 6.x admite tablas de uniones implícitas para relaciones muchos-a-muchos, pero no EF
Core).
La entidad CourseAssignment

Crear Models/CourseAssignment.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 CourseAssignment
{
public int InstructorID { get; set; }
public int CourseID { get; set; }
public Instructor Instructor { get; set; }
public Course Course { get; set; }
}
}

Unir nombres de entidad

Se requiere una tabla de unión en la base de datos para la relación Muchos a Muchos de
Instructor a cursos y debe ser representada por un conjunto de entidades. Es común
nombrar una entidad de unión EntityName1EntityName2 , que en este caso
sería CourseInstructor . Sin embargo, le recomendamos que elija un nombre que describa
la relación. Los modelos de datos empiezan simples y crecen y con frecuencia obteniendo
cargas útiles más tarde. Si empieza con un nombre de entidad descriptivo, no tendrá que
cambiar el nombre más tarde. Idealmente, la entidad de unión tendría su propio nombre
natural (posiblemente una sola palabra) en el dominio de negocio. Por ejemplo, los libros y
los clientes podrían vincularse mediante calificaciones. Para esta
relación, CourseAssignment es una mejor opción que CourseInstructor .
Clave compuesta

Dado que las claves ajenas no son anulables y, conjuntamente, identifican de forma única
cada fila de la tabla, no hay necesidad de una clave primaria
separada. Las propiedades InstructorID y CourseID deben funcionar como una clave primaria
compuesta. La única manera de identificar claves primarias compuestas a EF es utilizando
la API fluida (no se puede hacer mediante el uso de atributos). Verá cómo configurar la
clave primaria compuesta en la siguiente sección.

La clave compuesta asegura que mientras puede tener varias filas para un curso y varias
filas para un instructor, no puede tener varias filas para el mismo instructor y curso. La
entidad de unión Enrollment define su propia clave primaria, por lo que los duplicados de
este tipo son posibles. Para evitar estos duplicados, puede agregar un índice único en los
campos de clave externa o configurar Enrollment con una clave compuesta principal similar
a CourseAssignment .

Actualizar el contexto de la base de datos

Agregue el siguiente código resaltado al archivo Data/SchoolContext.cs :

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


}
}
}

Este código agrega las nuevas entidades y configura la clave primaria compuesta de la
entidad CourseAssignment.

Alternativa API fluida a los atributos

El código en el método OnModelCreating de la DbContext clase utiliza el fluido API para


configurar el comportamiento EF. El API se denomina "fluido" porque a menudo se utiliza
encadenando una serie de llamadas de método en una única sentencia, como en este
ejemplo de la documentación de EF Core :

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}

En este tutorial, utiliza la API fluida sólo para la asignación de bases de datos que no se
puede hacer con los atributos. Sin embargo, también puede utilizar la fluida API para
especificar la mayoría de las reglas de formato, validación y mapeo que puede hacer
utilizando atributos. Algunos atributos como MinimumLength no se pueden aplicar con la API
fluida. Como se mencionó anteriormente, MinimumLength no cambia el esquema, solo aplica
una regla de validación de cliente y servidor.

Algunos desarrolladores prefieren usar la API fluida exclusivamente para que puedan
mantener sus clases de entidad "limpias". Puede mezclar atributos y fluidez API si lo desea,
y hay algunas personalizaciones que sólo se pueden hacer utilizando fluido API, pero en
general la práctica recomendada es elegir uno de estos dos enfoques y utilizar de forma
coherente tanto como sea posible. Si utiliza ambos, tenga en cuenta que siempre que haya
un conflicto, Fluent API anula atributos.

Diagrama de entidad que muestra relaciones

La siguiente ilustración muestra el diagrama que el Entity Framework Power Tools crea para
el modelo de School completado.
Además de las líneas de relación uno-a-muchos (1 a *), puede ver aquí la línea de relación
de uno a cero o uno (1 a 0..1) entre las entidades Instructor y OfficeAssignment y la relación
cero o -una-a-muchas línea de relación (0..1 a *) entre las entidades del Instructor y del
Departamento.

Rellene la base de datos con datos de prueba

Reemplace el código en el archivo Data/DbInitializer.cs con el código siguiente para


proporcionar datos de semillas para las entidades nuevas que ha creado.

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("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.Students.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.Enrollments.Where(
s =>
s.Student.ID == e.StudentID &&
s.Course.CourseID == e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollments.Add(e);
}
}
context.SaveChanges();
}
}
}
Como se vio en el primer tutorial, la mayor parte de este código simplemente crea nuevos
objetos de entidad y carga los datos de muestra en las propiedades según sea necesario
para la prueba. Observe cómo se manejan las relaciones muchos-a-muchos: el código crea
relaciones creando entidades en los conjuntos de entidades
unidos Enrollments y CourseAssignment .

Agregar una migración

Guarde los cambios y compile el proyecto. A continuación, abra la ventana de comandos en


la carpeta del proyecto e introduzca el comando migrations add (no haga todavía el
comando update-database):

dotnet ef migrations add ComplexDataModel

Obtiene 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 intentó ejecutar el comando database update en este momento (no lo haga todavía),
obtendría el siguiente error:

La instrucción ALTER TABLE en conflicto con la restricción FOREIGN KEY


"FK_dbo.Course_dbo.Department_DepartmentID". El conflicto se produjo en la base de
datos "ContosoUniversity", tabla "dbo.Department", columna 'DepartmentID'.

A veces, cuando se ejecutan migraciones con datos existentes, es necesario insertar datos
de stub en la base de datos para satisfacer restricciones de clave externa. El código
generado en el método Up agrega una clave externa de DepartmentID no anulable a la
tabla Course. Si ya hay filas en la tabla de cursos cuando se ejecuta el código, la
operación AddColumn falla porque SQL Server no sabe qué valor poner en la columna que
no puede ser null. Para este tutorial ejecutarás la migración en una nueva base de datos,
pero en una aplicación de producción deberías hacer que la migración maneje los datos
existentes, por lo que las siguientes instrucciones muestran un ejemplo de cómo hacerlo.

Para que esta migración funcione con los datos existentes, hay que cambiar el código para
darle a la nueva columna un valor por defecto y crear un departamento de stub llamado
"Temp" para actuar como el departamento predeterminado. Como resultado, las filas del
curso existentes estarán relacionadas con el departamento "Temp" después de que se
ejecute el método Up .
• Abra el archivo {timestamp} _ComplexDataModel.cs .
• Comente 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 siguiente código resaltado después del código que crea la tabla
Department:

migrationBuilder.CreateTable(
name: "Department",
columns: table => new
{
DepartmentID = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy",
SqlServerValueGenerationStrategy.IdentityColumn),
Budget = table.Column<decimal>(type: "money", nullable: false),
InstructorID = table.Column<int>(nullable: true),
Name = table.Column<string>(maxLength: 50, nullable: true),
StartDate = table.Column<DateTime>(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);

En una aplicación en producción, escribiría código o secuencias de comandos para agregar


filas de departamento y relacionar las filas de curso con las nuevas filas de
departamento. Entonces ya no necesitará el departamento "Temp" o el valor
predeterminado en la columna Course.DepartmentID.

Guarde los cambios y cree el proyecto.

Cambiar la cadena de conexión y actualizar la base de datos

Ahora tiene un nuevo código en la DbInitializer clase que agrega datos de semillas para
las entidades nuevas a una base de datos vacía. Para que EF cree una nueva base de datos
vacía, cambie el nombre de la base de datos en la cadena de conexión en appsettings.json a
ContosoUniversity3 o algún otro nombre que no haya utilizado en el equipo que está
utilizando.

{
"ConnectionStrings": {
"DefaultConnection":
"Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;Mult
ipleActiveResultSets=true"
},

Guarde su cambio en appsettings.json .


Nota

Como alternativa al cambio del nombre de la base de datos, puede eliminar la base de
datos. Utilice el Explorador de objetos de SQL Server (SSOX) o el comando CLI database
drop :

dotnet ef database drop


Después de haber cambiado el nombre de la base de datos o haber eliminado la base de
datos, ejecute el database update comando en la ventana de comandos para ejecutar las
migraciones.

dotnet ef database update

Ejecute la aplicación para que el método DbInitializer.Initialize se ejecute y rellene la


nueva base de datos.

Abra la base de datos en SSOX como lo hizo anteriormente y expanda el nodo Tablas para
ver que todas las tablas han sido creadas. (Si aún tiene SSOX abierto desde la hora anterior,
haga clic en el botón Actualizar.)

Ejecute la aplicación para activar el código de inicialización que semilla la base de datos.

Haga clic con el botón secundario en la tabla CourseAssignment y seleccione Ver


datos para verificar que tiene datos en él.
+
Lectura de datos relacionados - EF Core con
ASP.NET Core MVC tutorial (6 of 10)
Objetivo: leer y mostrar los datos relacionados, es decir, los datos que el Entity
Framework carga en las propiedades de navegación.

Las siguientes ilustraciones muestran las páginas con las que trabajará.
Carga impaciente, explícita y perezosa de datos relacionados

Hay varias maneras en que el software Object-Relational Mapping (ORM) como Entity
Framework puede cargar datos relacionados en las propiedades de navegación de una
entidad:

• Carga impaciente (Eager). Cuando se lee la entidad, los datos relacionados se


recuperan junto con ella. Esto normalmente resulta en una consulta de unión única
que recupera todos los datos necesarios. Especifica la carga impaciente en Entity
Framework Core mediante los métodos Include y ThenInclude .

Puede recuperar algunos de los datos en consultas independientes y EF "corrige" las


propiedades de navegación. Es decir, EF agrega automáticamente las entidades
recuperadas por separado a las que pertenecen en propiedades de navegación de
entidades recuperadas previamente. Para la consulta que recupera datos relacionados,
puede utilizar el método Load en lugar de un método que devuelve una lista u objeto,
como ToList o Single .

• Carga explícita. Cuando se lee por primera vez la entidad, no se recuperan los datos
relacionados. Escribes un código que recupera los datos relacionados si es
necesario. Como en el caso de la carga impaciente con consultas independientes, la
carga explícita da como resultado múltiples consultas enviadas a la base de datos. La
diferencia es que con la carga explícita, el código especifica las propiedades de
navegación que se van a cargar. En Entity Framework Core 1.1 puede utilizar
el Load método para realizar la carga explícita. Por ejemplo:
• Carga perezosa. Cuando se lee por primera vez la entidad, no se recuperan los datos
relacionados. Sin embargo, la primera vez que intenta acceder a una propiedad de
navegación, los datos necesarios para esa propiedad de navegación se recuperan
automáticamente. Se envía una consulta a la base de datos cada vez que intenta
obtener datos de una propiedad de navegación por primera vez. Entity Framework
Core 1.0 no admite carga perezosa.

Consideraciones de rendimiento

Si sabe que necesita datos relacionados para cada entidad recuperada, la carga impaciente
a menudo ofrece el mejor rendimiento, ya que una sola consulta enviada a la base de datos
suele ser más eficiente que las consultas independientes para cada entidad recuperada. Por
ejemplo, supongamos que cada departamento tiene diez cursos relacionados. La carga
impaciente de todos los datos relacionados daría como resultado una sola consulta (unirse)
y un solo viaje de ida y vuelta a la base de datos. Una consulta independiente para los
cursos para cada departamento daría lugar a once viajes de ida y vuelta a la base de
datos. Los viajes de ida y vuelta extra a la base de datos son especialmente perjudiciales
para el rendimiento cuando la latencia es alta.

Por otro lado, en algunos casos, las consultas independientes son más eficientes. La carga
imprevista de todos los datos relacionados en una consulta puede provocar que se genere
una combinación muy compleja, que SQL Server no puede procesar de manera eficiente. O
si necesita acceder a las propiedades de navegación de una entidad sólo para un
subconjunto de un conjunto de entidades que está procesando, las consultas
independientes podrían funcionar mejor, ya que la carga impaciente de todo lo anterior
recuperaría más datos de los que necesita. Si el rendimiento es crítico, lo mejor es probar el
rendimiento en ambos sentidos para hacer la mejor elección.

Crear una página de cursos que muestre el nombre del departamento

La entidad de curso incluye una propiedad de navegación que contiene la entidad


Departamento del departamento al que se asigna el curso. Para mostrar el nombre del
departamento asignado en una lista de cursos, necesita obtener la propiedad Name de la
entidad Department que se encuentra en la propiedad de navegación Course.Department .
Cree un controlador denominado CourseController para el tipo de entidad de curso,
utilizando las mismas opciones para el controlador MVC con vistas, y la estructura de
Entity Framework que realizó anteriormente para el controlador Estudiantes, como se
muestra en la ilustración siguiente:

Abra CoursController.cs y examine el método Index . El andamio automático ha especificado


la carga impaciente para la propiedad de navegación Department usando el
método Include .

Reemplace el método Index con el código siguiente que utiliza un nombre más apropiado
para el IQueryable que devuelve entidades de Course ( courses en lugar
de schoolContext ):

public async Task<IActionResult> Index()


{
var courses = _context.Courses
.Include(c => c.Department)
.AsNoTracking();
return View(await courses.ToListAsync());
}

Abra Views/Courses/Index.cshtml y reemplace el código de plantilla con el código


siguiente. Los cambios están resaltados:

@model IEnumerable<ContosoUniversity.Models.Course>
@{
ViewData["Title"] = "Courses";
}

<h2>Courses</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>

<th>
@Html.DisplayNameFor(model => model.CourseID)
</th>

<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>

<td>
@Html.DisplayFor(modelItem => item.CourseID)
</td>

<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Credits)
</td>
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.CourseID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.CourseID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.CourseID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Se han realizado los siguientes cambios en el código de andamios:

• Cambió el encabezado de Index a Courses.


• Se agregó una columna Número que muestra el valor de la propiedad CourseID . De
forma predeterminada, las claves primarias no son scaffolded porque normalmente no
tienen sentido para los usuarios finales. Sin embargo, en este caso, la clave principal es
significativa y se desea mostrarla.
• Cambió la columna Departamento para mostrar el nombre del departamento. El
código muestra la propiedad Name de la entidad Departamento que se ha cargado en
la propiedad de navegación Department :

@Html.DisplayFor(modelItem => item.Department.Name)

Ejecute la página (seleccione la pestaña Courses en la página principal de Contoso


University) para ver la lista con los nombres de los departamentos.
Crear una página de Instructores que muestre Cursos e Inscripciones

En esta sección, creará un controlador y una vista para la entidad Instructor con el fin de
mostrar la página Instructores:
Esta página lee y muestra los datos relacionados de las siguientes maneras:
• La lista de instructores muestra los datos relacionados de la entidad
OfficeAssignment. Las entidades Instructor y OfficeAssignment están en una relación
de uno a cero o uno. Utilizará la carga impaciente para las entidades
OfficeAssignment. Como se explicó anteriormente, la carga impaciente suele ser más
eficiente cuando se necesitan los datos relacionados para todas las filas recuperadas
de la tabla principal. En este caso, desea mostrar asignaciones de oficina para todos
los instructores mostrados.
• Cuando el usuario selecciona un instructor, se muestran entidades del curso
relacionadas. Las entidades del instructor y del curso están en una relación muchos-a-
muchos. Utilizará la carga impaciente para las entidades del curso y sus entidades de
departamento relacionadas. En este caso, las consultas separadas pueden ser más
eficientes porque sólo se necesitan cursos para el instructor seleccionado. Sin
embargo, este ejemplo muestra cómo utilizar la carga impaciente para propiedades de
navegación dentro de las entidades que están en propiedades de navegación.
• Cuando el usuario selecciona un curso, se muestran los datos relacionados del
conjunto de entidades de Enrollments. Las entidades del curso y de la inscripción
están en una relación uno-a-muchos. Utilizará consultas independientes para las
entidades de inscripción y sus entidades de estudiante relacionadas.

Crear un modelo de vista para la vista Índice del instructor

La página Instructores muestra datos de tres tablas diferentes. Por lo tanto, creará un
modelo de vista que incluya tres propiedades, cada una de las cuales contenga los datos de
una de las tablas.

En la carpeta SchoolViewModels , cree InstructorIndexData.cs y reemplace el código existente


con el código siguiente:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
public class InstructorIndexData
{
public IEnumerable<Instructor> Instructors { get; set; }
public IEnumerable<Course> Courses { get; set; }
public IEnumerable<Enrollment> Enrollments { get; set; }
}
}
Crear el controlador de instructor y las vistas

Cree un controlador Instructors con acciones de lectura/escritura EF como se muestra en la


siguiente ilustración:

Abra InstructorsController.cs y agregue una instrucción using para el espacio de nombres


ViewModels:

using ContosoUniversity.Models.SchoolViewModels;

Reemplace el método Index con el código siguiente para realizar la carga impaciente de
datos relacionados y ponerlo en el modelo de la vista.

public async Task<IActionResult> Index(int? id, int? courseID)


{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}

return View(viewModel);
}

El método acepta datos de ruta opcionales ( id ) y un parámetro de cadena de consulta


( courseID ) que proporcionan los valores de ID del instructor seleccionado y del curso
seleccionado. Los parámetros se proporcionan mediante los hipervínculos Select en la
página.

El código comienza creando una instancia del modelo de la vista y poniendo en ella la lista
de instructores. El código especifica la carga impaciente para las propiedades de
navegación Instructor.OfficeAssignment y Instructor.CourseAssignments . Dentro de
la propiedad CourseAssignments , se carga la propiedad Course , y dentro de ella se cargan
ls propiedades Enrollments y Department , y dentro de cada entidd Enrollment
la Student propiedad es cargada.

viewModel.Instructors = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

Dado que la vista siempre requiere la entidad OfficeAssignment, es más eficiente buscarla
en la misma consulta. Las entidades del curso son necesarias cuando se selecciona un
instructor en la página web, por lo que una sola consulta es mejor que varias consultas sólo
si la página se muestra con más frecuentemente con un curso seleccionado que sin
ninguno.

El código se repite CourseAssignments y Course porque necesita dos propiedades


de Course . La primera cadena de llamadas a ThenInclude
recibe CourseAssignment.Course , Course.Enrollments y Enrollment.Student .

viewModel.Instructors = await _context.Instructors


.Include(i => i.OfficeAssignment)

.Include(i => i.CourseAssignments)


.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)

.ThenInclude(i => i.Student)


.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

En ese punto en el código, el siguiente ThenInclude sería para las propiedades de


navegación de Student , que no se necesitan. Pero llamar Include comienza con las
propiedades Instructor , por lo que tiene que pasar por la cadena de nuevo, esta vez
especificando Course.Department en lugar de Course.Enrollments .

viewModel.Instructors = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)

.Include(i => i.CourseAssignments)


.ThenInclude(i => i.Course)

.ThenInclude(i => i.Department)


.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

El código siguiente se ejecuta cuando se selecciona un instructor. El instructor seleccionado


se recupera de la lista de instructores en el modelo de la vista. La propiedad del modelo de
la vista Courses se carga con las entidades Course desde la propiedad de
navegación CourseAssignments del instructor .

if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

El método Where devuelve una colección, pero en este caso los criterios pasados a ese
método dan como resultado sólo una unica entidad Instructor que se devuelve. El
método Single convierte la colección en una única entidad Instructor, que le da acceso a la
propiedad CourseAssignments de esa entidad . La propiedad CourseAssignments contiene
entidades CourseAssignment , de las que sólo desea las entidades Course relacionadas.

Utilice el método Single en una colección cuando sabe que la colección tendrá sólo un
elemento. El método Single lanza una excepción si la colección pasada a ella está vacía o si
hay más de un elemento. Una alternativa es SingleOrDefault , que devuelve un valor
predeterminado (nulo en este caso) si la colección está vacía. Sin embargo, en este caso que
aún resultaría en una excepción (de intentar encontrar una propiedad Courses en una
referencia nula) y el mensaje de excepción indicaría menos claramente la causa del
problema. Cuando llama al método Single , también puede pasar la condición Where en
lugar de llamar al método Where por separado:

.Single(i => i.ID == id.Value)

En lugar de:

.Where(I => i.ID == id.Value).Single()


A continuación, si se selecciona un curso, el curso seleccionado se recupera de la lista de
cursos del modelo de la vista. A continuación, la propiedad Enrollments del modelo de la
vista se carga con las entidades de inscripción de la propiedad de navegación Enrollments
de ese curso .

if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}

Modificar la vista Índex del instructor

En Views/ Instructors / Index.cshtml , reemplace el código de plantilla con el código


siguiente. Los cambios están resaltados.

@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData

@{
ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>

<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>

<th>Courses</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Instructors)
{

string selectedRow = "";


if (item.ID == (int?)ViewData["InstructorID"])
{
selectedRow = "success";
}

<tr class="@selectedRow">
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.HireDate)
</td>

<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
<td>
@{
foreach (var course in item.CourseAssignments)
{
@course.Course.CourseID @: @course.Course.Title <br />
}
}

</td>
<td>
<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Has realizado los siguientes cambios en el código existente:

• Cambió la clase de modelo a InstructorIndexData .


• Cambió el título de la página de Index a Instructors .
• Se agregó una columna de Office que muestra item.OfficeAssignment.Location sólo
si item.OfficeAssignment no es null. (Debido a que esta es una relación de uno a cero o
uno, puede que no exista una entidad OfficeAssignment relacionada).

@if (item.OfficeAssignment != null)


{
@item.OfficeAssignment.Location
}

• Se agregó una columna de Courses que muestra los cursos impartidos por cada
instructor.
• Código añadido que agrega dinámicamente class="success" al elemento tr del
instructor seleccionado. Esto establece un color de fondo para la fila seleccionada
usando una clase Bootstrap.

string selectedRow = "";


if (item.ID == (int?)ViewData["InstructorID"])
{
selectedRow = "success";
}
<tr class="@selectedRow">

• Se agregó un nuevo hipervínculo denominado Select inmediatamente antes de los


otros vínculos de cada fila, lo que hace que el ID del instructor seleccionado sea
enviado al método Index .

<a asp-action="Index" asp-route-id="@item.ID">Select</a> |

Ejecute la aplicación y seleccione la ficha Instructores. La página muestra la propiedad


Ubicación de entidades OfficeAssignment relacionadas y una celda de tabla vacía cuando
no hay ninguna entidad OfficeAssignment relacionada.
En el archivo Views / Instructors / Index.cshtml , después del elemento de cierre de tabla (al
final del archivo), agregue el código siguiente. Este código muestra una lista de cursos
relacionados con un instructor cuando se selecciona un instructor.

@if (Model.Courses != null)


{
<h3>Courses Taught by Selected Instructor</h3>
<table class="table">
<tr>
<th></th>
<th>Number</th>
<th>Title</th>
<th>Department</th>
</tr>

@foreach (var item in Model.Courses)


{
string selectedRow = "";
if (item.CourseID == (int?)ViewData["CourseID"])
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID
})
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}

</table>
}
Este código lee la propiedad Courses del modelo de la vista para mostrar una lista de
cursos. También proporciona un hipervínculo Select que envía el ID del curso seleccionado
al método de acción Index .

Ejecute la página y seleccione un instructor. Ahora ves una cuadrícula que muestra los
cursos asignados al instructor seleccionado, y para cada curso ves el nombre del
departamento asignado.

Después del bloque de código que acaba de agregar, agregue el código siguiente. Esto
muestra una lista de los estudiantes que están matriculados en un curso cuando se
selecciona ese curso.
@if (Model.Enrollments != null)
{
<h3>
Students Enrolled in Selected Course
</h3>
<table class="table">
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}

Este código lee la propiedad Inscripciones del modelo de vista para mostrar una lista de los
estudiantes matriculados en el curso.

Ejecute la página y seleccione un instructor. Ahora ves una cuadrícula que muestra los
cursos asignados al instructor seleccionado, y para cada curso ves el nombre del
departamento asignado.
Después del bloque de código que acaba de agregar, agregue el código siguiente. Esto
muestra una lista de los estudiantes que están matriculados en un curso cuando se
selecciona ese curso.

@if (Model.Enrollments != null)


{
<h3>
Students Enrolled in Selected Course
</h3>
<table class="table">
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}

Este código lee la propiedad Inscripciones del modelo de vista para mostrar una lista de los
estudiantes matriculados en el curso.

Ejecute la página y seleccione un instructor. Luego seleccione un curso para ver la lista de
estudiantes matriculados y sus calificaciones.
Carga explícita

Cuando recuperó la lista de instructores en InstructorsController.cs , especificó la carga


impaciente para la propiedad de navegación CourseAssignments .

Suponga que esperaba que los usuarios rara vez quisieran ver las inscripciones en un
instructor y curso seleccionados. En ese caso, es posible que desee cargar los datos de
inscripción sólo si se solicita. Para ver un ejemplo de cómo realizar la carga explícita,
reemplace el método Index con el código siguiente, que elimina la carga impaciente para
las inscripciones y carga dicha propiedad explícitamente. Los cambios de código están
resaltados.

public async Task<IActionResult> Index(int? id, int? courseID)


{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.OrderBy(i => i.LastName)
.ToListAsync();

if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;

var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();


await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
foreach (Enrollment enrollment in selectedCourse.Enrollments)
{
await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
}

viewModel.Enrollments = selectedCourse.Enrollments;
}
return View(viewModel);
}

El nuevo código elimina las llamadas al método ThenInclude para los datos de inscripción
del código que recupera las entidades del instructor. Si se selecciona un instructor y un
curso, el código resaltado recupera entidades de inscripción para el curso seleccionado y
entidades de estudiante para cada inscripción.

Ejecute la página de índice del instructor ahora y verá que no hay diferencia en lo que se
muestra en la página, aunque ha cambiado la forma en que se recuperan los datos.
Actualización de datos relacionados - EF Core
con ASP.NET Core MVC tutorial (7 of 10)
Objetivo: actualización de datos relacionados actualizando los campos de clave
externa y las propiedades de navegación.

Las siguientes ilustraciones muestran algunas de las páginas con las que trabajará.
Personalizar la creación y edición de páginas para cursos

Cuando se crea una nueva entidad de curso, debe tener una relación con un departamento
existente. Para facilitar esto, el código de andamio incluye métodos de controlador y vistas
de Crear y Editar que incluyen una lista desplegable para seleccionar el departamento. La
lista desplegable establece la propiedad Course.DepartmentID de clave externa y todas las
necesidades de Entity Framework para cargar la propiedad de navegación Department con
la entidad de departamento adecuada. Utilizará el código de scaffold, pero lo cambiará
ligeramente para agregar el tratamiento de errores y ordenar la lista desplegable.
En CoursesController.cs , elimine los cuatro métodos Create y Edit y reemplácelos por el
siguiente código:

public IActionResult Create()


{
PopulateDepartmentsDropDownList();
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("CourseID,Credits,DepartmentID,Title")]
Course course)
{
if (ModelState.IsValid)
{
_context.Add(course);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}

var course = await _context.Courses


.AsNoTracking()
.SingleOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
if (id == null)
{
return NotFound();
}
var courseToUpdate = await _context.Courses
.SingleOrDefaultAsync(c => c.CourseID == id);

if (await TryUpdateModelAsync<Course>(courseToUpdate,
"",
c => c.Credits, c => c.DepartmentID, c => c.Title))
{
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
return RedirectToAction(nameof(Index));
}
PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID);
return View(courseToUpdate);
}

Después del método HttpPost Edit , cree un método nuevo que cargue información de
departamento para la lista desplegable.

private void PopulateDepartmentsDropDownList(object selectedDepartment = null)


{
var departmentsQuery = from d in _context.Departments
orderby d.Name
select d;
ViewBag.DepartmentID = new SelectList(departmentsQuery.AsNoTracking(),
"DepartmentID", "Name", selectedDepartment);
}

El método PopulateDepartmentsDropDownList obtiene una lista de todos los departamentos


ordenados por nombre, crea una colección SelectList para una lista desplegable y pasa la
colección a la vista en ViewBag . El método acepta el parámetro opcional
selectedDepartment que permite al código de llamada especificar el elemento que se
seleccionará cuando se muestre la lista desplegable. La vista pasará el nombre
"DepartmentID" al ayudante de la etiqueta <select> , y el ayudante entonces sabe mirar en
el objeto ViewBag para un SelectList que se llame “DepartmentID".
El método HttpGet Create llama al método PopulateDepartmentsDropDownList sin
determinar el elemento seleccionado, ya que para un nuevo curso el departamento aún no
se ha establecido:

public IActionResult Create()


{
PopulateDepartmentsDropDownList();
return View();
}

El método HttpGet Edit establece el elemento seleccionado, basado en el ID del


departamento que ya está asignado al curso que se está editando:

public async Task<IActionResult> Edit(int? id)


{
if (id == null)
{
return NotFound();
}

var course = await _context.Courses


.AsNoTracking()
.SingleOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}

Los métodos HttpPost para ambos Create y Edit también incluyen código que establece el
elemento seleccionado cuando vuelven a mostrar la página después de un error. Esto
asegura que cuando se vuelva a mostrar la página para mostrar el mensaje de error, el
departamento seleccionado se mantiene seleccionado.

Agregar .AsNoTracking a los métodos de Details y Delete

Para optimizar el rendimiento de los detalles del curso y eliminar páginas, agregue
llamadas AsNoTracking en los métodos HttpGet Details y Delete .

public async Task<IActionResult> Details(int? id)


{
if (id == null)
{
return NotFound();
}

var course = await _context.Courses


.Include(c => c.Department)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}

return View(course);
}
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}

var course = await _context.Courses


.Include(c => c.Department)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}

return View(course);
}

Modificar las vistas del curso

En Views / Courses / Create.cshtml , agregue una opción "Select Departament" a la lista


desplegable DepartamentID , cambie el título de DepartmentID a Departament y agregue
un mensaje de validación.

<div class="form-group">

<label asp-for="Department" class="control-label"></label>


<select asp-for="DepartmentID" class="form-control" asp-items="ViewBag.DepartmentID">
<option value="">-- Select Department --</option>
</select>

<span asp-validation-for="DepartmentID" class="text-danger" />

En Views / Courses / Edit.cshtml , realice el mismo cambio para el campo Departamento que
acaba de hacer en Create.cshtml .

También en Views / Courses / Edit.cshtml , agregue un campo de número de curso antes


del campo Títle . Dado que el número de curso es la clave principal, se muestra, pero no se
puede cambiar.

<div class="form-group">
<label asp-for="CourseID" class="control-label"></label>
<div>@Html.DisplayFor(model => model.CourseID)</div>
</div>

Ya hay un campo oculto ( <input type="hidden"> ) para el número de curso en la vista


Edit. La adición de un ayudante de etiquetas <label> no elimina la necesidad del campo
oculto porque no hace que el número de curso se incluya en los datos publicados cuando
el usuario hace clic en Save en la página Edit .

En Views / Courss / Delete.cshtml , agregue un campo de número de curso en la parte


superior y cambie el ID del departamento al nombre del departamento.

@model ContosoUniversity.Models.Course

@{
ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>


<div>
<h4>Course</h4>
<hr />
<dl class="dl-horizontal">

<dt>
@Html.DisplayNameFor(model => model.CourseID)
</dt>
<dd>
@Html.DisplayFor(model => model.CourseID)

</dd>
<dt>
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd>
@Html.DisplayFor(model => model.Title)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Credits)
</dt>
<dd>
@Html.DisplayFor(model => model.Credits)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Name)
</dd>
</dl>

<form asp-action="Delete">
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-action="Index">Back to List</a>
</div>
</form>
</div>

En Views / Courses / Details.cshtml , realice el mismo cambio que acaba de hacer


para Delete.cshtml .

Pruebe las páginas del curso

Ejecute la aplicación, selecciones la pestaña Courses y haga clic en Create new, e


introuduzca datos para un nuevo curso:
Haga clic en Create . La página de índice de cursos se muestra con el nuevo curso añadido
a la lista. El nombre del departamento en la lista de la página de índice proviene de la
propiedad de navegación, mostrando que la relación se estableció correctamente.

Ejecute la página Editar (haga clic en Edit en un curso en la página Índice de cursos).
Cambie los datos de la página y haga clic en Save . La página Índice de cursos se muestra
con los datos del curso actualizados.

Agregar una página de edición para los instructores

Cuando edita un registro de instructor, desea poder actualizar la asignación de la oficina del
instructor. La entidad Instructor tiene una relación de uno a cero o uno con la entidad
OfficeAssignment, lo que significa que su código debe manejar las siguientes situaciones:

• Si el usuario borra la asignación de oficina y originalmente tenía un valor, elimine la


entidad OfficeAssignment.
• Si el usuario introduce un valor de asignación de oficina y originalmente estaba vacío,
cree una nueva entidad OfficeAssignment.
• Si el usuario cambia el valor de una asignación de oficina, cambie el valor en una
entidad OfficeAssignment existente.

Actualizar el controlador Instructors

En InstructorsController.cs , cambie el código en el método HttpGet Edit para que cargue


la propiedad de navegación OfficeAssignment de la entidad Instructor y las
llamadas AsNoTracking :

public async Task<IActionResult> Edit(int? id)


{
if (id == null)
{
return NotFound();
}

var instructor = await _context.Instructors


.Include(i => i.OfficeAssignment)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.ID == id);
if (instructor == null)
{
return NotFound();
}
return View(instructor);
}

Reemplace el método HttpPost Edit con el código siguiente para controlar las
actualizaciones de asignación de oficina:

[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
if (id == null)
{
return NotFound();
}

var instructorToUpdate = await _context.Instructors


.Include(i => i.OfficeAssignment)
.SingleOrDefaultAsync(s => s.ID == id);

if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
{
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
return RedirectToAction(nameof(Index));
}
return View(instructorToUpdate);
}

El código hace lo siguiente:

• Cambia el nombre del método EditPost porque la firma es ahora la misma que el
método HttpGet Edit (el atributo ActionName especifica que /Edit/ se sigue
utilizando la dirección URL).
• Obtiene la entidad Instructor actual de la base de datos mediante la carga impaciente
de la propiedad de navegación OfficeAssignment . Esto es lo mismo que se hizo en el
método HttpGet Edit .
• Actualiza la entidad Instructor recuperada con valores del Model dinder. La sobrecarga
de TryUpdateModel le permite incluir en la lista las propiedades que desea incluir. Esto
evita la sobreposición, como se explica anteriormente en el segundo apartado .

if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i =>
i.OfficeAssignment))

• Si la ubicación de la oficina está en blanco, establece la propiedad


Instructor.OfficeAssignment como null para que se elimine la fila relacionada en la
tabla OfficeAssignment.
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}

• Guarda los cambios en la base de datos.

Actualizar la vista Edit del Instructor

En Views / Instructors / Edit.cshtml , agregue un nuevo campo para editar la ubicación de la


oficina, al final antes del botón Save :

<div class="form-group">
<label asp-for="OfficeAssignment.Location" class="control-label"></label>
<input asp-for="OfficeAssignment.Location" class="form-control" />
<span asp-validation-for="OfficeAssignment.Location" class="text-danger" />
</div>

Ejecute la página (seleccione la pestaña Instructors y luego haga clic en Edit en un


instructor). Cambie la ubicación de Office y haga clic en Save .
Agregar asignaciones de cursos a la página Edit del instructor

Los instructores pueden enseñar cualquier número de cursos. Ahora mejorará la página de
edición del instructor agregando la capacidad de cambiar asignaciones de cursos mediante
un grupo de casillas de verificación, como se muestra en la siguiente captura de pantalla:
La relación entre el curso y las entidades del instructor es many-to-many. Para agregar y
quitar relaciones, agrega y elimina entidades hacia y desde el conjunto de entidades join de
CourseAssignments.

La interfaz de usuario que le permite cambiar los cursos a los que se asigna un instructor es
un grupo de casillas de verificación. Se muestra una casilla de verificación para cada curso
en la base de datos y se seleccionan las que están asignadas actualmente al instructor. El
usuario puede seleccionar o desmarcar las casillas de verificación para cambiar las
asignaciones del curso. Si el número de cursos era mucho mayor, probablemente desearía
utilizar un método diferente para presentar los datos en la vista, pero usaría el mismo
método de manipulación de una entidad join para crear o eliminar relaciones.

Actualizar el controlador de Instructorer

Para proporcionar datos a la vista de la lista de casillas de verificación, utilizará una clase de
modelo de la vista.

Cree AssignedCourseData.cs en la carpeta SchoolViewModels y reemplace el código existente


con el código siguiente:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
public class AssignedCourseData
{
public int CourseID { get; set; }
public string Title { get; set; }
public bool Assigned { get; set; }
}
}

En InstructorsController.cs , reemplace el método HttpGet Edit con el código siguiente. Los


cambios están resaltados.

public async Task<IActionResult> Edit(int? id)


{
if (id == null)
{
return NotFound();
}

var instructor = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments).ThenInclude(i => i.Course)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.ID == id);
if (instructor == null)
{
return NotFound();
}
PopulateAssignedCourseData(instructor);
return View(instructor);
}

private void PopulateAssignedCourseData(Instructor instructor)

{
var allCourses = _context.Courses;
var instructorCourses = new HashSet<int>(instructor.CourseAssignments.Select(c =>
c.CourseID));
var viewModel = new List<AssignedCourseData>();
foreach (var course in allCourses)
{
viewModel.Add(new AssignedCourseData
{
CourseID = course.CourseID,
Title = course.Title,
Assigned = instructorCourses.Contains(course.CourseID)
});
}
ViewData["Courses"] = viewModel;
}

El código agrega carga impaciente para la propiedad de navegación Courses y llama al


nuevo método PopulateAssignedCourseData para proporcionar información para la matriz de
casilla de verificación utilizando la clase AssignedCourseData de modelo de la vista.

El código en el método PopulateAssignedCourseData lee todas las entidades del curso para
cargar una lista de cursos utilizando la clase de modelo de la vista. Para cada curso, el
código comprueba si el curso existe en la propiedad de navegación Courses del
instructor . Para crear una búsqueda eficiente al comprobar si un curso se asigna al
instructor, los cursos asignados al instructor se ponen en una colección HashSet . La
propiedad Assigned se establece en true para los cursos a los que el instructor está
asignado. La vista utilizará esta propiedad para determinar qué casillas de verificación
deben mostrarse según se selecciona. Finalmente, la lista se pasa a la vista en ViewData .

A continuación, agregue el código que se ejecuta cuando el usuario hace clic en el


botón Save . Reemplace el método EditPost con el código siguiente y agregue un nuevo
método que actualiza la propiedad de navegación Courses de la entidad Instructor.

[HttpPost]
[ValidateAntiForgeryToken]

public async Task<IActionResult> Edit(int? id, string[] selectedCourses)

{
if (id == null)
{
return NotFound();
}

var instructorToUpdate = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.SingleOrDefaultAsync(m => m.ID == id);

if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
{
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
return RedirectToAction(nameof(Index));
}

UpdateInstructorCourses(selectedCourses, instructorToUpdate);

PopulateAssignedCourseData(instructorToUpdate);
return View(instructorToUpdate);
}

private void UpdateInstructorCourses(string[] selectedCourses, Instructor


instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}

var selectedCoursesHS = new HashSet<string>(selectedCourses);


var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new CourseAssignment {
InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
}
}
else
{

if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove =
instructorToUpdate.CourseAssignments.SingleOrDefault(i => i.CourseID == course.CourseID);
_context.Remove(courseToRemove);
}
}
}

La firma del método ahora es diferente del método HttpGet Edit , por lo que el nombre
del método cambia de nuevo de EditPost a Edit .

Dado que la vista no tiene una colección de entidades de curso, el model binder no puede
actualizar automáticamente la propiedad de navegación CourseAssignments . En lugar de
utilizar el model binder para actualizar la propiedad de navegación CourseAssignments , lo
hace en el nuevo método UpdateInstructorCourses . Por lo tanto, es necesario excluir la
propiedad CourseAssignments del model binding. Esto no requiere ningún cambio en el
código que llama a TryUpdateModel porque está utilizando la sobrecarga de listas
y CourseAssignments no está en la lista de inclusión.

Si no se seleccionaron casillas de verificación, el código


en UpdateInstructorCourses inicializa la propiedad de navegación CourseAssignments con
una colección vacía y devuelve:

private void UpdateInstructorCourses(string[] selectedCourses, Instructor


instructorToUpdate)
{

if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;

var selectedCoursesHS = new HashSet<string>(selectedCourses);


var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new CourseAssignment {
InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
}
}
else
{

if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove =
instructorToUpdate.CourseAssignments.SingleOrDefault(i => i.CourseID ==
course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}

El código pasa a lo largo de todos los cursos de la base de datos y comprueba cada curso
con respecto a los asignados actualmente al instructor en comparación con los que se
seleccionaron en la vista. Para facilitar búsquedas eficaces, las dos últimas colecciones se
almacenan en objetos HashSet .

Si se ha seleccionado la casilla de verificación de un curso pero el curso no está en la


propiedad de navegación Instructor.CourseAssignments , el curso se agrega a la colección
en la propiedad de navegación.

private void UpdateInstructorCourses(string[] selectedCourses, Instructor


instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}

var selectedCoursesHS = new HashSet<string>(selectedCourses);


var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
foreach (var course in _context.Courses)
{

if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new CourseAssignment {
InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
}

}
else
{

if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove =
instructorToUpdate.CourseAssignments.SingleOrDefault(i => i.CourseID ==
course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}

Si no se ha seleccionado la casilla de verificación de un curso, pero el curso está en la


propiedad de navegación Instructor.CourseAssignments , el curso se elimina de la
propiedad de navegación.

private void UpdateInstructorCourses(string[] selectedCourses, Instructor


instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}

var selectedCoursesHS = new HashSet<string>(selectedCourses);


var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new CourseAssignment {
InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
}
}

else
{

if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove =
instructorToUpdate.CourseAssignments.SingleOrDefault(i => i.CourseID == course.CourseID);
_context.Remove(courseToRemove);
}

}
}
}

Actualizar las vistas del instructor

En Views / Instructors / Edit.cshtml , agregue un campo Courses con una matriz de casillas
de verificación agregando el código siguiente inmediatamente después de los
elementos div para el campo de Office y antes del elemento div para el botón Save .
Nota

Al pegar el código en Visual Studio, los saltos de línea se cambiarán de una manera que
rompe el código. Presione Ctrl + Z una vez para deshacer el formato automático. Esto
solucionará los saltos de línea para que se vean como lo que ves aquí. La sangría no tiene
que ser perfecto, pero el @</tr><tr> , @:<td> , @:</td> , y @:</tr> las líneas deben estar,
cada uno en una sola línea como se muestra a continuación o que obtendrá un error de
ejecución. Con el bloque de código nuevo seleccionado, presione Tab tres veces para
alinear el nuevo código con el código existente.

<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<table>
<tr>
@{
int cnt = 0;
List<ContosoUniversity.Models.SchoolViewModels.AssignedCourseData>
courses = ViewBag.Courses;

foreach (var course in courses)


{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ? "checked=\"checked\"" :
"")) />
@course.CourseID @: @course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>

Este código crea una tabla HTML que tiene tres columnas. En cada columna hay una casilla
de verificación seguida de un subtítulo que consiste en el número y título del curso. Las
casillas de verificación tienen el mismo nombre ("selectedCourses"), que informa al model
binder que deben ser tratados como un grupo. El atributo de valor de cada casilla de
verificación se establece en el valor de CourseID . Cuando se publica la página, el model
binder pasa una matriz al controlador que consiste en los valores CourseID de las casillas de
verificación seleccionadas.

Cuando las casillas de verificación se renderizan inicialmente, las que son para los cursos
asignados al instructor han comprobado los atributos, que los selecciona (los muestra
comprobados).

Ejecute la página Índice del instructor y haga clic en Edit en un instructor para ver la página.
Cambie algunas asignaciones de cursos y haga clic en Guardar. Los cambios que realice se
reflejan en la página de índice.
Nota

El enfoque adoptado aquí para editar los datos del curso del instructor funciona bien
cuando hay un número limitado de cursos. Para las colecciones que son mucho mayores, se
necesitaría una interfaz de usuario diferente y un método de actualización diferente.
Actualizar la página Eliminar

En InstructorsController.cs , elimine el método DeleteConfirmed e inserte el código siguiente


en su lugar.

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{

Instructor instructor = await _context.Instructors


.Include(i => i.CourseAssignments)

.SingleAsync(i => i.ID == id);

var departments = await _context.Departments


.Where(d => d.InstructorID == id)
.ToListAsync();

departments.ForEach(d => d.InstructorID = null);

_context.Instructors.Remove(instructor);

await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}

Este código realiza los cambios siguientes:

• Hace la carga inmediata para la propiedad de navegación CourseAssignments . Debe


incluir esto o EF no sabrá acerca de las entidades CourseAssignment relacionadas y no
los eliminará. Para evitar la necesidad de leerlos aquí, puede configurar la eliminación
en cascada en la base de datos.
• Si el instructor a borrar se asigna como administrador de cualquier departamento,
elimina la asignación del instructor de esos departamentos.
Añade la ubicación de la oficina y los cursos a la página Crear

En InstructorsController.cs , elimine los métodos HttpGet y HttpPost Create y , a


continuación, agregue el código siguiente en su lugar:

public IActionResult Create()


{

var instructor = new Instructor();


instructor.CourseAssignments = new List<CourseAssignment>();

PopulateAssignedCourseData(instructor);
return View();
}

// POST: Instructors/Create
[HttpPost]
[ValidateAntiForgeryToken]

public async Task<IActionResult>


Create([Bind("FirstMidName,HireDate,LastName,OfficeAssignment")] Instructor instructor,
string[] selectedCourses)

if (selectedCourses != null)
{
instructor.CourseAssignments = new List<CourseAssignment>();
foreach (var course in selectedCourses)
{
var courseToAdd = new CourseAssignment { InstructorID = instructor.ID, CourseID
= int.Parse(course) };
instructor.CourseAssignments.Add(courseToAdd);
}

}
if (ModelState.IsValid)
{
_context.Add(instructor);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
PopulateAssignedCourseData(instructor);
return View(instructor);
}

Este código es similar a lo que vio para los métodos Edit , excepto que inicialmente no se
seleccionan cursos. El método HttpGet Create llama al
método PopulateAssignedCourseData no porque podría haber cursos seleccionados sino
para para proporcionar una colección vacía para el bucle foreach en la vista (de lo contrario
el código de la vista lanzaría una excepción de referencia nula).

El método HttpPost Create añade cada curso seleccionado a la propiedad de


navegación CourseAssignments antes de comprobar los errores de validación y agrega el
nuevo instructor a la base de datos. Los cursos se añaden incluso si hay errores de modelo
para que cuando haya errores de modelo (por ejemplo, el usuario introduzca una fecha no
válida) y la página se vuelva a mostrar con un mensaje de error, todas las selecciones de
cursos que se hayan realizado se restaurarán automáticamente.

Tenga en cuenta que para poder añadir cursos a la propiedad de


navegación CourseAssignments tiene que inicializar la propiedad como una colección vacía:

instructor.CourseAssignments = new List<CourseAssignment>();

Como alternativa a hacerlo en el código del controlador, puede hacerlo en el modelo


Instructor cambiando el getter de propiedad para crear automáticamente la colección si no
existe, como se muestra en el ejemplo siguiente:

private ICollection<CourseAssignment> _courseAssignments;


public ICollection<CourseAssignment> CourseAssignments
{
get
{
return _courseAssignments ?? (_courseAssignments = new
List<CourseAssignment>());
}
set
{
_courseAssignments = value;
}
}

Si modifica la propiedad CourseAssignments de esta manera, puede quitar el código


explícito de inicialización de propiedad en el controlador.
En Views / Instructor / Create.cshtml , agregue un cuadro de texto de ubicación de oficina y
marque las casillas de los cursos antes del botón Enviar. Como en el caso de la página de
edición, corrija el formato si Visual Studio reformatea el código cuando lo pega .

<div class="form-group">
<label asp-for="OfficeAssignment.Location" class="control-label"></label>
<input asp-for="OfficeAssignment.Location" class="form-control" />
<span asp-validation-for="OfficeAssignment.Location" class="text-danger" />
</div>

<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<table>
<tr>
@{
int cnt = 0;
List<ContosoUniversity.Models.SchoolViewModels.AssignedCourseData>
courses = ViewBag.Courses;

foreach (var course in courses)


{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ? "checked=\"checked\"" :
"")) />
@course.CourseID @: @course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>

Pruebe la aplicación agregando un instructor.


Manejo de transacciones

Entity Framework implícitamente implementa transacciones. Para escenarios donde necesite


más control, por ejemplo, si desea incluir operaciones realizadas fuera de Entity Framework
en una transacción, consulte Transacciones .
Manejo de conflictos de concurrencia - EF
Core con ASP.NET Core MVC tutorial (8 of
10)
Objetivo: Manejar errores de concurrencia cuando varios usuarios actualizan la misma
entidad al mismo tiempo.

Las siguientes ilustraciones muestran las páginas Editar y Eliminar, incluyendo algunos
mensajes que se muestran si se produce un conflicto de concurrencia.
Conflictos de concurrencia

Se produce un conflicto de concurrencia cuando un usuario muestra los datos de una


entidad para editarlos y, a continuación, otro usuario actualiza los datos de la misma
entidad antes de que se escriba el cambio del primer usuario en la base de datos. Si no
habilita la detección de dichos conflictos, quien actualiza la última base de datos
sobrescribe los cambios del otro usuario. En muchas aplicaciones, este riesgo es aceptable:
si hay pocos usuarios, o pocas actualizaciones, o si no es realmente crítico si algunos
cambios se sobrescriben, el costo de la programación para la concurrencia podría
compensar el beneficio. En ese caso, no tiene que configurar la aplicación para controlar
conflictos de simultaneidad.

Concurrencia pesimista (bloqueo)

Si su aplicación necesita evitar la pérdida accidental de datos en escenarios de concurrencia,


una forma de hacerlo es usar bloqueos de base de datos. Esto se llama concurrencia
pesimista. Por ejemplo, antes de leer una fila de una base de datos, solicita un bloqueo para
sólo lectura o para actualizar el acceso. Si bloquea una fila para el acceso de actualización,
no se permite a ningún otro usuario bloquear la fila para el acceso de sólo lectura o de
actualización, ya que obtendría una copia de los datos que están en proceso de
modificación. Si bloquea una fila para acceso de sólo lectura, otros también pueden
bloquearla para acceso de sólo lectura, pero no para actualizar.

La administración de bloqueos tiene desventajas. Puede ser complejo de


programar. Requiere recursos significativos de administración de bases de datos y puede
causar problemas de rendimiento a medida que aumenta el número de usuarios de una
aplicación. Por estas razones, no todos los sistemas de administración de bases de datos
soportan concurrencia pesimista. Entity Framework Core no proporciona soporte
incorporado para él, y este tutorial no muestra cómo implementarlo.

Concurrencia optimista

La alternativa a la concurrencia pesimista es la concurrencia optimista. La simultaneidad


optimista significa permitir que los conflictos de concurrencia ocurran, y luego reaccionar
apropiadamente si lo hacen. Por ejemplo, Jane visita la página de Edición de Departamento
y cambia la cantidad del Presupuesto para el departamento de Inglés de $ 350,000.00 a $
0.00.
Antes de que Jane haga clic en Save para guardar los cambios, John visita la misma página
y cambia el campo Fecha de inicio del 9/1/2007 al 9/1/2013.
Jane hace clic en Save primero y ve su cambio cuando el navegador regresa a la página de
índice.
Entonces Juan hace clic en Save en una página de Edición que todavía muestra un
presupuesto de $ 350,000.00. Y lo que sucede a continuación depende de cómo maneje los
conflictos de concurrencia.

Algunas de las opciones incluyen lo siguiente:

• Puede realizar un seguimiento de la propiedad que un usuario ha modificado y


actualizar sólo las columnas correspondientes en la base de datos.

En el escenario de ejemplo, no se perderían datos, ya que los dos usuarios


actualizaron diferentes propiedades. La próxima vez que alguien navegue por el
departamento de Inglés, verán los cambios de Jane y John - una fecha de inicio de
9/1/2013 y un presupuesto de cero dólares. Este método de actualización puede
reducir el número de conflictos que podrían resultar en la pérdida de datos, pero no
puede evitar la pérdida de datos si se hacen cambios competitivos a la misma
propiedad de una entidad. Si el Entity Framework funciona de esta manera depende
de cómo implemente el código de actualización. A menudo no es práctico en una
aplicación web, ya que puede requerir que mantenga grandes cantidades de estado
con el fin de realizar un seguimiento de todos los valores de propiedad original para
una entidad, así como nuevos valores.

• Puede dejar que el cambio de John sobre-escriba el cambio de Jane.

La próxima vez que alguien navegue por el departamento de Inglés, verá el 9/1/2013
y el valor restaurado de $ 350,000.00. Esto se conoce como escenario el ultimo es el
que gana . (Todos los valores tienen prioridad sobre lo que hay en el almacén de
datos.) Como se indicó en la introducción a esta sección, si no hace ninguna
codificación para el manejo de la simultaneidad, esto sucederá automáticamente.
• Puede evitar que el cambio de John se actualice en la base de datos.

Normalmente, se muestra un mensaje de error, mostrarle el estado actual de los


datos, y le permite volver a aplicar sus cambios si todavía quiere hacerlos. Esto se
conoce como escenario de Gana el Almacenamiento. (Los valores del almacén de
datos tienen prioridad sobre los valores enviados.) Implementarás el escenario
Almacén de las tiendas en este tutorial. Este método asegura que no se sobrescriben
los cambios sin que un usuario sea alertado de lo que está sucediendo.

Detección de conflictos de concurrencia

Puede resolver conflictos mediante el manejo de excepciones DbConcurrencyException que


Entity Framework genera. Para saber cuándo lanzar estas excepciones, el Entity Framework
debe ser capaz de detectar conflictos. Por lo tanto, debe configurar la base de datos y el
modelo de datos de forma adecuada. Algunas opciones para habilitar la detección de
conflictos son las siguientes:

• En la tabla de base de datos, incluya una columna de seguimiento que se puede


utilizar para determinar cuándo se ha cambiado una fila. A continuación, puede
configurar Entity Framework para incluir esa columna en la cláusula Where de los
comandos SQL Update o Delete.

El tipo de datos de la columna de seguimiento es típicamente rowversion. El


valor rowversion es un número secuencial que se incrementa cada vez que se
actualiza la fila. En un comando Actualizar o Borrar, la cláusula Where incluye el valor
original de la columna de seguimiento (la versión de fila original). Si la fila que se
está actualizando ha sido cambiada por otro usuario, el valor de la
columna rowversion es diferente al valor original, por lo que la sentencia Actualizar o
Eliminar no puede encontrar la fila para actualizar debido a la cláusula
Where. Cuando el Entity Framework detecta que ninguna fila ha sido actualizada por
el comando Actualizar o Eliminar (es decir, cuando el número de filas afectadas es
cero), lo interpreta como un conflicto de simultaneidad.

• Configure el Entity Framework para incluir los valores originales de cada columna de
la tabla en la cláusula Where de los comandos Actualizar y Eliminar.

Al igual que en la primera opción, si algo en la fila ha cambiado desde que se leyó
por primera vez la fila, la cláusula Where no devolverá una fila para actualizar, que
Entity Framework interpreta como un conflicto de simultaneidad. Para tablas de base
de datos que tienen muchas columnas, este enfoque puede resultar en cláusulas
Where muy grandes y puede requerir que mantenga grandes cantidades de
estado. Como se señaló anteriormente, el mantenimiento de grandes cantidades de
estado puede afectar el rendimiento de la aplicación. Por lo tanto, este enfoque
generalmente no se recomienda, y no es el método utilizado en este tutorial.
Si desea implementar este enfoque para la simultaneidad, debe marcar todas las
propiedades de clave no primaria en la entidad que desea controlar la simultaneidad
agregando el atributo ConcurrencyCheck a ellas. Ese cambio permite que Entity
Framework incluya todas las columnas en la cláusula SQL Where de las sentencias
Update y Delete.

En el resto de este tutorial agregará una propiedad rowversion de seguimiento a la entidad


Departamento, creará un controlador y vistas y comprobará que todo funciona
correctamente.

Agregar una propiedad de seguimiento a la entidad de departamento

En Models / Department.cs , agregue una propiedad de seguimiento denominada


RowVersion:

[! code-csharp Principal ]

#define Final
#if Begin

#region snippet_Begin
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; }
}
}
#endregion

#elif Final
#region snippet_Final
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; }

[Timestamp]
public byte[] RowVersion { get; set; }

public Instructor Administrator { get; set; }


public ICollection<Course> Courses { get; set; }
}
}
#endregion
#endif

El atributo Timestamp especifica que esta columna se incluirá en la cláusula Where de los
comandos Update y Delete enviados a la base de datos. El atributo se denomina Timestamp
porque las versiones anteriores de SQL Server utilizaban un tipo de datos timestamp antes
de que rowversion lo reemplazara. El tipo .NET para rowversion es una matriz de bytes.
Si prefiere usar la API fluida, puede utilizar el método IsConcurrencyToken (en Data /
SchoolContext.cs ) para especificar la propiedad de seguimiento, como se muestra en el
ejemplo siguiente:

modelBuilder.Entity < Departamento > ()


. Propiedad (p => p.RowVersion). IsConcurrencyToken ();

Al agregar una propiedad cambió el modelo de base de datos, por lo que necesita realizar
otra migración.

Guarde los cambios y cree el proyecto e introduzca los siguientes comandos en la ventana
de comandos:

dotnet ef migrations añadir RowVersion

dotnet ef database update


Crear un controlador de Departamentos y vistas

Andamia un controlador de Departamentos y puntos de vista como lo hiciste antes para


Estudiantes, Cursos e Instructores.

En el archivo DepartmentsController.cs , cambie las cuatro apariciones de "FirstMidName" a


"FullName" para que las listas desplegables del administrador del departamento
contengan el nombre completo del instructor en lugar de sólo el apellido.

ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName",


department.InstructorID);

Actualizar la vista Índice de departamentos

El motor de andamios creó una columna RowVersion en la vista de índice, pero ese campo
no debe mostrarse.

Reemplace el código en Views / Departments / Index.cshtml con el código siguiente.

@model IEnumerable<ContosoUniversity.Models.Department>

@{
ViewData["Title"] = "Departments";
}

<h2>Departments</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Administrator)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Budget)
</td>
<td>
@Html.DisplayFor(modelItem => item.StartDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Administrator.FullName)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.DepartmentID">Edit</a> |
<a asp-action="Details" asp-route-
id="@item.DepartmentID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Esto cambia el encabezado a "Departamentos", elimina la columna RowVersion y muestra el


nombre completo en lugar del nombre para el administrador.

Actualizar los métodos de edición en el controlador Departamentos

En el método HttpGet Edit y el método Details, agregue AsNoTracking. En el


método HttpGet Edit, agregue carga anticipada para el administrador.

var department = await _context.Departments


.Include(i => i.Administrator)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.DepartmentID == id);

Reemplace el código existente para el método HttpPost Edit con el código siguiente:

[HttpPost]

[ValidateAntiForgeryToken]

public async Task<IActionResult> Edit(int? id, byte[] rowVersion)

if (id == null)

return NotFound();

var departmentToUpdate = await _context.Departments.Include(i =>


i.Administrator).SingleOrDefaultAsync(m => m.DepartmentID == id);

if (departmentToUpdate == null)

Department deletedDepartment = new Department();

await TryUpdateModelAsync(deletedDepartment);
ModelState.AddModelError(string.Empty,

"Unable to save changes. The department was deleted by another user.");

ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID",


"FullName", deletedDepartment.InstructorID);

return View(deletedDepartment);

_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue =
rowVersion;

if (await TryUpdateModelAsync<Department>(

departmentToUpdate,

"",

s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))

try

await _context.SaveChangesAsync();

return RedirectToAction(nameof(Index));

catch (DbUpdateConcurrencyException ex)

var exceptionEntry = ex.Entries.Single();

var clientValues = (Department)exceptionEntry.Entity;

var databaseEntry = exceptionEntry.GetDatabaseValues();

if (databaseEntry == null)

ModelState.AddModelError(string.Empty,

"Unable to save changes. The department was deleted by another


user.");

else

{
var databaseValues = (Department)databaseEntry.ToObject();

if (databaseValues.Name != clientValues.Name)

ModelState.AddModelError("Name", $"Current value:


{databaseValues.Name}");

if (databaseValues.Budget != clientValues.Budget)

ModelState.AddModelError("Budget", $"Current value:


{databaseValues.Budget:c}");

if (databaseValues.StartDate != clientValues.StartDate)

ModelState.AddModelError("StartDate", $"Current value:


{databaseValues.StartDate:d}");

if (databaseValues.InstructorID != clientValues.InstructorID)

Instructor databaseInstructor = await


_context.Instructors.SingleOrDefaultAsync(i => i.ID == databaseValues.InstructorID);

ModelState.AddModelError("InstructorID", $"Current value:


{databaseInstructor?.FullName}");

ModelState.AddModelError(string.Empty, "The record you attempted to edit


"

+ "was modified by another user after you got the original


value. The "

+ "edit operation was canceled and the current values in the


database "

+ "have been displayed. If you still want to edit this record,


click "

+ "the Save button again. Otherwise click the Back to List


hyperlink.");
departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;

ModelState.Remove("RowVersion");

ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName",


departmentToUpdate.InstructorID);

return View(departmentToUpdate);

El código comienza intentando leer el departamento para ser actualizado. Si el


método SingleOrDefaultAsync devuelve null, el departamento fue eliminado por otro
usuario. En ese caso, el código utiliza los valores de formulario publicados para crear una
entidad de departamento para que la página Editar pueda volver a mostrarse con un
mensaje de error. Como alternativa, no tendría que volver a crear la entidad de
departamento si sólo muestra un mensaje de error sin volver a mostrar los campos de
departamento.

La vista almacena el valor original RowVersion en un campo oculto y este método recibe ese
valor en el parámetro rowVersion. Antes de llamar SaveChanges, tiene que poner ese valor de
la propiedad RowVersion original en la colección OriginalValues para la entidad.

_contexto. Entrada (departmentToUpdate). Propiedad ( " RowVersion " ) .OriginalValue =


rowVersion;

A continuación, cuando Entity Framework crea un comando SQL UPDATE, ese comando
incluirá una cláusula WHERE que busque una fila que tenga el valor original RowVersion. Si
ninguna fila se ve afectada por el comando UPDATE (ninguna fila tiene el
valor original RowVersion), Entity Framework genera una
excepción DbUpdateConcurrencyException.

El código en el bloque catch para esa excepción obtiene la entidad Departamento afectada
que tiene los valores actualizados de la propiedad Entries en el objeto excepción.

var exceptionEntry = ex.Entries.Single();

La colección Entries tendrá sólo un objeto EntityEntry. Puede utilizar ese objeto para
obtener los nuevos valores introducidos por el usuario y los valores de la base de datos
actual.
var clientValues = (Department)exceptionEntry.Entity;

var databaseEntry = exceptionEntry.GetDatabaseValues();

El código agrega un mensaje de error personalizado para cada columna que tiene valores
de base de datos diferentes de lo que el usuario introdujo en la página Editar (sólo un
campo se muestra aquí para abreviar).

var databaseValues = (Department)databaseEntry.ToObject();

if (databaseValues.Name != clientValues.Name)

ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");

Finalmente, el código establece el valor RowVersion del nuevo valor departmentToUpdate


recuperado de la base de datos. Este nuevo valor RowVersion se almacenará en el campo
oculto cuando se vuelva a mostrar la página Editar y la próxima vez que el usuario haga clic
en Save , sólo se detectarán errores de concurrencia que ocurran desde que se vuelve a
mostrar la página Editar.

departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");

La instrucción ModelState.Remove es obligatoria porque ModelState tiene el valor antiguo


de RowVersion. En la vista, el valor ModelState de un campo tiene prioridad sobre los valores
de propiedad del modelo cuando ambos están presentes.

Actualizar la vista Departamento Editar

En Views / Departments / Edit.cshtml , realice los siguientes cambios:

• Agregue un campo oculto para guardar el valor de la propiedad RowVersion,


inmediatamente después del campo oculto de la propiedad DepartmentID.

• Agregue una opción "Seleccionar administrador" a la lista desplegable.

@model ContosoUniversity.Models.Department

@{

ViewData["Title"] = "Edit";

}
<h2>Edit</h2>

<h4>Department</h4>

<hr />

<div class="row">

<div class="col-md-4">

<form asp-action="Edit">

<div asp-validation-summary="ModelOnly" class="text-danger"></div>

<input type="hidden" asp-for="DepartmentID" />


<input type="hidden" asp-for="RowVersion" />
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Budget" class="control-label"></label>
<input asp-for="Budget" class="form-control" />
<span asp-validation-for="Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StartDate" class="control-label"></label>
<input asp-for="StartDate" class="form-control" />
<span asp-validation-for="StartDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="InstructorID" class="control-label"></label>
<select asp-for="InstructorID" class="form-control" asp-
items="ViewBag.InstructorID">
<option value="">-- Select Administrator --</option>
</select>
<span asp-validation-for="InstructorID" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
</div>

<div>
<a asp-action="Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Prueba de conflictos de concurrencia en la página Editar

Ejecute el sitio y haga clic en Departamentos para ir a la página Índice de departamentos.

Haga clic con el botón derecho en el hipervínculo Edit para el departamento de inglés y
seleccione Abrir en nueva pestaña y , a continuación, haga clic en el hipervínculo Edit para
el departamento de inglés. Las dos pestañas del navegador ahora muestran la misma
información.

Cambie un campo en la primera pestaña del navegador y haga clic en Save .


El navegador muestra la página de índice con el valor cambiado.

Cambie un campo en la segunda pestaña del navegador.


Haga clic en Save . Aparecerá un mensaje de error:
Haga clic en Save de nuevo. El valor introducido en la segunda pestaña del navegador se
guardará. Verá los valores guardados cuando aparezca la página de índice.

Actualizar la página Eliminar

Para la página Eliminar, Entity Framework detecta conflictos de concurrencia causados por
otra persona que edita el departamento de una manera similar. Cuando el
método HttpGet Delete muestra la vista de confirmación, la vista incluye el
valor original RowVersion en un campo oculto. Ese valor está entonces disponible para el
método HttpPost Delete que se llama cuando el usuario confirma la eliminación. Cuando el
Entity Framework crea el comando SQL DELETE, incluye una cláusula WHERE con el
valor original RowVersion. Si el comando da como resultado cero filas afectadas (lo que
significa que la fila se ha cambiado después de que se mostró la página de confirmación
Eliminar), se produce una excepción de simultaneidad y el método HttpGet Delete se llama
con un indicador de error a true para volver a mostrar la página de confirmación con un
mensaje de error. También es posible que cero filas se hayan visto afectadas porque la fila
fue eliminada por otro usuario, por lo que en ese caso no se muestra ningún mensaje de
error.

Actualizar los métodos Delete en el controlador Departments

En DepartmentsController.cs , reemplace el método HttpGet Delete con el código siguiente:

public async Task<IActionResult> Delete(int? id, bool? concurrencyError)


{
if (id == null)
{
return NotFound();
}

var department = await _context.Departments


.Include(d => d.Administrator)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.DepartmentID == id);
if (department == null)
{
if (concurrencyError.GetValueOrDefault())
{
return RedirectToAction(nameof(Index));
}
return NotFound();
}

if (concurrencyError.GetValueOrDefault())
{
ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
+ "was modified by another user after you got the original values. "
+ "The delete operation was canceled and the current values in the "
+ "database have been displayed. If you still want to delete this "
+ "record, click the Delete button again. Otherwise "
+ "click the Back to List hyperlink.";
}
return View(department);
}

El método acepta un parámetro opcional que indica si la página se vuelve a mostrar


después de un error de concurrencia. Si este indicador es verdadero y el departamento
especificado ya no existe, fue eliminado por otro usuario. En ese caso, el código redirige a la
página de índice. Si este indicador es verdadero y el Departamento existe, fue cambiado por
otro usuario. En ese caso, el código envía un mensaje de error a la vista usando ViewData.
Reemplace el código en el método HttpPost Delete (que se llama DeleteConfirmed) con el
código siguiente:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(Department department)
{
try
{
if (await _context.Departments.AnyAsync(m => m.DepartmentID ==
department.DepartmentID))
{
_context.Departments.Remove(department);
await _context.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
catch (DbUpdateConcurrencyException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction(nameof(Delete), new { concurrencyError = true, id =
department.DepartmentID });
}
}

En el código de andamios que acaba de reemplazar, este método aceptó sólo un ID de


registro:

public async Tarea < IActionResult > DeleteConfirmed ( int id )

Ha cambiado este parámetro a una instancia de entidad de departamento creada por el


model binder. Esto proporciona EF acceso al valor de propiedad RowVersion además de la
clave de registro.

public async Tarea < IActionResult > Eliminar ( Departamento de departamento )


También ha cambiado el nombre del método de acción de DeleteConfirmed a Delete. El
código de andamio usó el nombre DeleteConfirmed para dar al método HttpPost una firma
única. Ahora que las firmas son únicas, puede quedarse con la convención MVC y usar el
mismo nombre para los métodos de eliminación HttpPost y HttpGet.
Si el departamento ya se ha eliminado, el método AnyAsync devuelve false y la aplicación
vuelve al método Index.
Si se detecta un error de simultaneidad, el código vuelve a mostrar la página de
confirmación Eliminar y proporciona un indicador que indica que debe mostrar un mensaje
de error de simultaneidad.

Actualizar la vista Eliminar

En Views / Departments / Delete.cshtml , reemplace el código de scaffolded con el código


siguiente que agrega un campo de mensaje de error y campos ocultos para las propiedades
DepartmentID y RowVersion. Los cambios están resaltados.

@model ContosoUniversity.Models.Department

@{

ViewData["Title"] = "Delete";

<h2>Delete</h2>

<p class="text-danger">@ViewData["ConcurrencyErrorMessage"]</p>

<h3>Are you sure you want to delete this?</h3>


<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Budget)
</dt>
<dd>
@Html.DisplayFor(model => model.Budget)
</dd>
<dt>
@Html.DisplayNameFor(model => model.StartDate)
</dt>
<dd>
@Html.DisplayFor(model => model.StartDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Administrator)
</dt>
<dd>
@Html.DisplayFor(model => model.Administrator.FullName)
</dd>
</dl>

<form asp-action="Delete">
<input type="hidden" asp-for="DepartmentID" />
<input type="hidden" asp-for="RowVersion" />
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-action="Index">Back to List</a>
</div>
</form>
</div>

Esto hace los cambios siguientes:

• Agrega un mensaje de error entre los encabezados h2 y h3.

• Reemplaza FirstMidName con FullName en el campo Administrador .

• Quita el campo RowVersion.

• Agrega un campo oculto para la propiedad RowVersion.

Ejecute la página Índice de departamentos. Haga clic derecho en


el hipervínculo Delete para el departamento de Inglés y seleccione Abrir en nueva
pestaña , luego en la primera pestaña haga clic en el hipervínculo Edit para el
departamento de Inglés.

En la primera ventana, cambie uno de los valores y haga clic en Save :


En la segunda ficha, haga clic en Delete . Verá el mensaje de error de la simultaneidad y los
valores de Departamento se actualizarán con lo que está actualmente en la base de datos.
Si hace clic en Delete de nuevo, se redirigirá a la página de índice, que muestra que se ha
eliminado el departamento.

Actualizar detalles y crear vistas

Opcionalmente puede limpiar el código de andamio en las vistas Details y Create.

Reemplace el código en Views / Departaments / Details.cshtml para eliminar la columna


RowVersion y muestre el nombre completo del Administrador.

@model ContosoUniversity.Models.Department
@{

ViewData["Title"] = "Details";

<h2>Details</h2>

<div>

<h4>Department</h4>

<hr />

<dl class="dl-horizontal">

<dt>

@Html.DisplayNameFor(model => model.Name)

</dt>

<dd>

@Html.DisplayFor(model => model.Name)

</dd>

<dt>

@Html.DisplayNameFor(model => model.Budget)

</dt>

<dd>

@Html.DisplayFor(model => model.Budget)

</dd>

<dt>

@Html.DisplayNameFor(model => model.StartDate)

</dt>

<dd>

@Html.DisplayFor(model => model.StartDate)

</dd>

<dt>

@Html.DisplayNameFor(model => model.Administrator)

</dt>

<dd>
@Html.DisplayFor(model => model.Administrator.FullName)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.DepartmentID">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>

Reemplace el código en Views / Departamenos / Create.cshtml para agregar una opción


Seleccionar a la lista desplegable.
Herencia - EF Core con ASP.NET Core MVC
tutorial (9 de 10)

En la programación orientada a objetos, puede utilizar la herencia para facilitar la


reutilización del código. En este tutorial, cambiará las clases Instructor y Student de modo
que deriven de una clase Person base que contenga propiedades como LastName que son
comunes a los instructores y estudiantes. No agregará ni cambiará ninguna página web,
pero cambiará parte del código y esos cambios se reflejarán automáticamente en la base de
datos.

Opciones para asignar herencia a tablas de base de datos

Las clases Instructor y Student en el modelo de datos de la escuela tienen varias


propiedades que son idénticas:

Supongamos que desea eliminar el código redundante para las propiedades que son
compartidas por las entidades Instructor y Student . O usted quiere escribir un servicio que
puede dar formato a nombres sin importar si el nombre vino de un instructor o de un
estudiante. Puede crear una clase Person base que contenga sólo las propiedades
compartidas, y luego hacer que las clases Instructor y Student hereden de esa clase base,
como se muestra en la siguiente ilustración:
Hay varias maneras en que esta estructura de herencia podría ser representada en la base
de datos. Usted podría tener una tabla de personas que incluye información sobre los
estudiantes y los instructores en una sola tabla. Algunas de las columnas podrían aplicarse
sólo a los instructores (HireDate), algunos sólo a los estudiantes (EnrollmentDate), algunos a
ambos (LastName, FirstName). Normalmente, tendría una columna de discriminador para
indicar qué tipo de fila representa. Por ejemplo, la columna del discriminador podría tener
"Instructor" para los instructores y "Estudiante" para los estudiantes.

Este patrón de generación de una estructura de herencia de entidad desde una única tabla
de base de datos se denomina herencia de tabla por herencia (TPH).
Una alternativa es hacer que la base de datos se parezca más a la estructura de
herencia. Por ejemplo, podría tener sólo los campos de nombre en la tabla Person y tener
tablas separadas de Instructor y de Estudiante con los campos de fecha.

Este patrón de creación de una tabla de base de datos para cada clase de entidad se
denomina herencia de tabla por tipo (TPT).

Otra opción es asignar todos los tipos no abstractos a tablas individuales. Todas las
propiedades de una clase, incluidas las propiedades heredadas, se asignan a las columnas
de la tabla correspondiente. Este patrón se denomina herencia de Tabla por Hormigón
(TPC). Si implementó la herencia de TPC para las clases Persona, Estudiante e Instructor
como se muestra anteriormente, las tablas de Estudiante e Instructor no tendrían ningún
aspecto diferente después de implementar la herencia que antes.

Los patrones de herencia TPC y TPH suelen ofrecer un mejor rendimiento que los patrones
de herencia TPT, ya que los patrones TPT pueden resultar en consultas de unión complejas.

Este tutorial muestra cómo implementar la herencia TPH. TPH es el único patrón de
herencia que soporta el Entity Framework Core. Lo que harás es crear una clase Person ,
cambiar las clases Instructor y Student para derivar de Person , añadir la nueva clase al
DbContext y crear una migración.

Crear la clase Person

En la carpeta Models, cree Person.cs y reemplace el código de plantilla con el código


siguiente:
-+
/*using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public abstract class Person
{
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; }

[Display(Name = "Full Name")]


public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
}
}

Hacer que las clases de estudiante e instructor hereden de la persona

En Instructor.cs , herede la clase Instructor de la clase Person y quite los campos de clave y
nombre. El código se verá como el ejemplo siguiente:

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

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

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


public OfficeAssignment OfficeAssignment { get; set; }
}
}

Realice los mismos cambios en Student.cs .

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

namespace ContosoUniversity.Models
{
public class Student : Person
{
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode =
true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }

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


}
}

Agregue el tipo de entidad Person al modelo de datos

Agregue el tipo de entidad Persons a SchoolContext.cs . Las nuevas líneas se resaltan.

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; }
public DbSet<Person> People { 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<Person>().ToTable("Person");

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

Esto es todo lo que el Entity Framework necesita para configurar la herencia de tabla por
jerarquía. Como verá, cuando se actualice la base de datos, tendrá una tabla Person en
lugar de las tablas de Estudiante e Instructor.

Crear y personalizar el código de migración

Guarde los cambios y cree el proyecto. A continuación, abra la ventana de comandos en la


carpeta del proyecto e introduzca el siguiente comando:

dotnet ef migrations add Inheritance

No ejecute el comando database update todavía, porque se producirá una perdida datos, ya
que eliminará la tabla Instructor y cambiará el nombre de la tabla Estudiante a
Persona. Debe proporcionar código personalizado para conservar los datos existentes.
Abra Migrations / <timestamp> _Inheritance.cs y reemplace el método Up con el código
siguiente:

protected override void Up(MigrationBuilder migrationBuilder)


{
migrationBuilder.DropForeignKey(
name: "FK_Enrollment_Student_StudentID",
table: "Enrollment");

migrationBuilder.DropIndex(name: "IX_Enrollment_StudentID", table: "Enrollment");

migrationBuilder.RenameTable(name: "Instructor", newName: "Person");


migrationBuilder.AddColumn<DateTime>(name: "EnrollmentDate", table: "Person",
nullable: true);
migrationBuilder.AddColumn<string>(name: "Discriminator", table: "Person", nullable:
false, maxLength: 128, defaultValue: "Instructor");
migrationBuilder.AlterColumn<DateTime>(name: "HireDate", table: "Person", nullable:
true);
migrationBuilder.AddColumn<int>(name: "OldId", table: "Person", nullable: true);

// Copy existing Student data into new Person table.


migrationBuilder.Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate,
EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, null AS HireDate,
EnrollmentDate, 'Student' AS Discriminator, ID AS OldId FROM dbo.Student");
// Fix up existing relationships to match new PK's.
migrationBuilder.Sql("UPDATE dbo.Enrollment SET StudentId = (SELECT ID FROM
dbo.Person WHERE OldId = Enrollment.StudentId AND Discriminator = 'Student')");

// Remove temporary key


migrationBuilder.DropColumn(name: "OldID", table: "Person");

migrationBuilder.DropTable(
name: "Student");

migrationBuilder.CreateIndex(
name: "IX_Enrollment_StudentID",
table: "Enrollment",
column: "StudentID");

migrationBuilder.AddForeignKey(
name: "FK_Enrollment_Person_StudentID",
table: "Enrollment",
column: "StudentID",
principalTable: "Person",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
}
Este código se encarga de las siguientes tareas de actualización de la base de datos:

• Elimina las restricciones de clave externa y los índices que apuntan a la tabla
Estudiante.
• Cambia el nombre de la tabla Instructor como persona y realiza los cambios
necesarios para almacenar los datos del estudiante:
• Agrega la fecha de inscripción anulable para los estudiantes.
• Agrega la columna Discriminador para indicar si una fila es para un estudiante o un
instructor.
• Hace HireDate anulable ya que las filas de estudiantes no tendrán fechas de alquiler.
• Agrega un campo temporal que se utilizará para actualizar claves ajenas que apuntan
a los estudiantes. Al copiar a los estudiantes en la tabla de personas obtendrán nuevos
valores de clave primaria.
• Copia los datos de la tabla Estudiante en la tabla Persona. Esto hace que los
estudiantes reciban nuevos valores de clave primaria asignados.
• Corrige valores clave ajenas que apuntan a los estudiantes.
• Reconstruye restricciones de claves externas e índices, ahora apuntándolos a la tabla
Person.

(Si utilizó GUID en lugar de entero como tipo de clave principal, los valores de clave
primaria de estudiante no tendrían que cambiar y varios de estos pasos podrían haberse
omitido).

Ejecute el omando database update :

dotnet ef database update

(En un sistema en producción haría los cambios correspondientes en el método Down en


caso de que tuviera que usarlo para regresar a la versión anterior de la base de datos. Para
este tutorial no va a usar el método Down .)
Nota

Es posible obtener otros errores al realizar cambios de esquema en una base de datos que
tiene datos existentes. Si obtiene errores de migración que no puede resolver, puede
cambiar el nombre de la base de datos en la cadena de conexión o eliminar la base de
datos. Con una nueva base de datos, no hay datos que migrar, y es más probable que el
comando update-database se complete sin errores. Para eliminar la base de datos, utilice
SSOX o ejecute el comando CLI database drop .

Prueba con herencia implementada

Ejecutar el sitio y probar varias páginas. Todo funciona igual que antes.
En el Explorador de objetos de SQL Server , expanda Conexiones de datos /
SchoolContext y, a continuación , Tablas y verá que las tablas de Student y Instructor han
sido reemplazadas por una tabla Person. Abra el diseñador de tabla de personas y verá que
tiene todas las columnas que solía estar en las tablas de Student y Instructor.

Haga clic con el botón secundario en la tabla Persona y, a continuación, haga clic
en Mostrar datos de tabla para ver la columna de discriminador.
Temas avanzados - Tutorial de EF Core con
ASP.NET Core MVC (10 de 10)
Objetivo: tratar varios temas que son útiles para tener en cuenta cuando se va más
allá de los conceptos básicos de desarrollo de aplicaciones web ASP.NET Core que
utilizan Entity Framework Core.

Consultas SQL sin procesar

Una de las ventajas de usar el Entity Framework es que evita atar su código demasiado de
cerca a un método particular de almacenar datos. Lo hace generando consultas SQL y
comandos para usted, que también le libera de tener que escribirlos. Pero hay escenarios
excepcionales cuando necesita ejecutar consultas SQL específicas que haya creado
manualmente. Para estos escenarios, la API de Entity Framework Code First incluye
métodos que permiten pasar comandos SQL directamente a la base de datos. Tiene las
siguientes opciones en EF Core 1.0:

• Utilice el método DbSet.FromSql para las consultas que devuelven tipos de


entidad. Los objetos devueltos deben ser del tipo esperado por el objeto DbSet y son
rastreados automáticamente por el contexto de la base de datos a menos
que desactive el seguimiento .
• Utilice los comandos Database.ExecuteSqlCommand for non-query.

Si necesita ejecutar una consulta que devuelva tipos que no sean entidades, puede utilizar
ADO.NET con la conexión de base de datos proporcionada por EF. Los datos devueltos no
son rastreados por el contexto de la base de datos, incluso si utiliza este método para
recuperar tipos de entidad.

Como es siempre cierto cuando ejecuta comandos SQL en una aplicación web, debe tomar
precauciones para proteger su sitio contra los ataques de inyección de SQL. Una forma de
hacerlo es utilizar consultas parametrizadas para asegurarse de que las cadenas enviadas
por una página web no pueden interpretarse como comandos SQL. En este tutorial utilizará
consultas parametrizadas al integrar la entrada del usuario en una consulta.

Llamar a una consulta que devuelve entidades

La clase DbSet<TEntity> proporciona un método que puede utilizar para ejecutar una
consulta que devuelve una entidad de tipo TEntity . Para ver cómo funciona esto, cambiará
el código en el método Details del controlador del Departamento.
En DepartmentsController.cs , en el método Details , reemplace el código que recupera un
departamento con una llamada de método FromSql , como se muestra en el siguiente
código resaltado:

public async Task<IActionResult> Details(int? id)


{
if (id == null)
{
return NotFound();
}

string query = "SELECT * FROM Department WHERE DepartmentID = {0}";


var department = await _context.Departments
.FromSql(query, id)
.Include(d => d.Administrator)
.AsNoTracking()
.SingleOrDefaultAsync();

if (department == null)
{
return NotFound();
}

return View(department);
}

Para comprobar que el nuevo código funciona correctamente, seleccione


la ficha Departaments y, a continuación, Details para uno de los departamentos.
Llamar a una consulta que devuelva otros tipos

Anteriormente creó una cuadrícula de estadísticas de alumnos para la página Acerca de la


que mostraba el número de estudiantes para cada fecha de inscripción. Usted obtuvo los
datos del conjunto de entidades Students ( _context.Students ) y usó LINQ para proyectar
los resultados en una lista de objetos EnrollmentDateGroup del modelo de la
vista. Supongamos que desea escribir su propia instrucción SQL en lugar de usar LINQ. Para
ello debe ejecutar una consulta SQL que devuelva algo distinto de los objetos entidad. En
EF Core 1.0, una forma de hacerlo es escribir ADO.NET código y obtener la conexión de
base de datos de EF.

En HomeController.cs , reemplace el método About con el código siguiente:

public async Task<ActionResult> About()


{

List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();


var conn = _context.Database.GetDbConnection();
try
{
await conn.OpenAsync();
using (var command = conn.CreateCommand())
{
string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
+ "FROM Person "
+ "WHERE Discriminator = 'Student' "
+ "GROUP BY EnrollmentDate";
command.CommandText = query;
DbDataReader reader = await command.ExecuteReaderAsync();

if (reader.HasRows)
{
while (await reader.ReadAsync())
{
var row = new EnrollmentDateGroup { EnrollmentDate =
reader.GetDateTime(0), StudentCount = reader.GetInt32(1) };
groups.Add(row);
}
}
reader.Dispose();
}
}
finally
{
conn.Close();
}

return View(groups);
}

Agregue una sentencia using:

using System.Data.Common;

Ejecute la página Acerca de. Muestra los mismos datos que antes.
Llamar a una consulta de actualización

Suponga que los administradores de Contoso University quieren realizar cambios globales
en la base de datos, como cambiar el número de créditos para cada curso. Si la universidad
tiene un gran número de cursos, sería ineficiente recuperarlos todos como entidades y
cambiarlos individualmente. En esta sección implementarás una página web que permite al
usuario especificar un factor para cambiar el número de créditos para todos los cursos y
realizar el cambio ejecutando una sentencia SQL UPDATE. La página web se verá como la
siguiente ilustración:

En CoursesContoller.cs , agregue métodos UpdateCourseCredits para HttpGet y HttpPost:

public IActionResult UpdateCourseCredits()


{
return View();
}

[HttpPost]
public async Task<IActionResult> UpdateCourseCredits(int? multiplier)
{
if (multiplier != null)
{
ViewData["RowsAffected"] =
await _context.Database.ExecuteSqlCommandAsync(
"UPDATE Course SET Credits = Credits * {0}",
parameters: multiplier);
}
return View();
}

Cuando el controlador procesa una solicitud HttpGet, ViewData["RowsAffected"] no


devuelve nada y la vista muestra un cuadro de texto vacío y un botón de envío, como se
muestra en la ilustración anterior.

Cuando se hace clic en el botón Update , se llama al método HttpPost y el multiplicador


tiene el valor introducido en el cuadro de texto. El código entonces ejecuta el SQL que
actualiza cursos y devuelve el número de filas afectadas a la vista en ViewData . Cuando la
vista obtiene un valor RowsAffected , muestra el número de filas actualizadas.

En el Explorador de soluciones , haga clic con el botón secundario en la carpeta Vistas /


Cursos y, a continuación, haga clic en Agregar> Nuevo elemento .

En el cuadro de diálogo Agregar nuevo elemento , haga clic en ASP.NET en Instalado en


el panel izquierdo, haga clic en Página de vista de MVC y el nombre de la nueva
vista UpdateCourseCredits.cshtml .

En Views / Courses / UpdateCourseCredits.cshtml , reemplace el código de plantilla con el


código siguiente:

@{
ViewBag.Title = "UpdateCourseCredits";
}

<h2>Update Course Credits</h2>

@if (ViewData["RowsAffected"] == null)


{
<form asp-action="UpdateCourseCredits">
<div class="form-actions no-color">
<p>
Enter a number to multiply every course's credits by:
@Html.TextBox("multiplier")
</p>
<p>
<input type="submit" value="Update" class="btn btn-default" />
</p>
</div>
</form>
}
@if (ViewData["RowsAffected"] != null)
{
<p>
Number of rows updated: @ViewData["RowsAffected"]
</p>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>

Ejecute el método UpdateCourseCredits seleccionando la ficha Cursos y añadiendo "/


UpdateCourseCredits" al final de la URL en la barra de direcciones del navegador (por
ejemplo:) http://localhost:5813/Courses/UpdateCourseCredits . Introduzca un número en el
cuadro de texto:

Haga clic en Update . Verá el número de filas afectadas:


Haga clic en Back to the list para ver la lista de cursos con el número revisado de créditos.

Tenga en cuenta que el código en producción garantizaría que las actualizaciones siempre
dan como resultado datos válidos. El código simplificado mostrado aquí podría multiplicar
el número de créditos lo suficiente como para resultar en números mayores de 5. (La
propiedad Credits tiene un atributo [Range(0, 5)] .) La consulta de actualización
funcionaría, pero los datos no válidos podrían causar resultados inesperados en otras partes
del sistema que asumen el número de créditos es 5 o menos.

Examinar SQL enviado a la base de datos

A veces es útil poder ver las consultas SQL reales que se envían a la base de datos. La
funcionalidad de registro integrada para ASP.NET Core es utilizada automáticamente por EF
Core para escribir registros que contienen SQL para consultas y actualizaciones. En esta
sección verá algunos ejemplos de registro SQL.

Abra StudentsController.cs y en el método Details establezca un punto de interrupción en


la sentencia if (student == null) .

Ejecute la aplicación en modo de depuración y vaya a la página Detalles de un estudiante.

Vaya a la ventana Salida que muestra la salida de depuración y verá la consulta:

Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (56ms)


[Parameters=[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [s].[ID], [s].[Discriminator], [s].[FirstName], [s].[LastName],
[s].[EnrollmentDate]
FROM [Person] AS [s]
WHERE ([s].[Discriminator] = N'Student') AND ([s].[ID] = @__id_0)
ORDER BY [s].[ID]
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (122ms)
[Parameters=[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT [s.Enrollments].[EnrollmentID], [s.Enrollments].[CourseID],
[s.Enrollments].[Grade], [s.Enrollments].[StudentID], [e.Course].[CourseID],
[e.Course].[Credits], [e.Course].[DepartmentID], [e.Course].[Title]
FROM [Enrollment] AS [s.Enrollments]
INNER JOIN [Course] AS [e.Course] ON [s.Enrollments].[CourseID] = [e.Course].[CourseID]
INNER JOIN (
SELECT TOP(1) [s0].[ID]
FROM [Person] AS [s0]
WHERE ([s0].[Discriminator] = N'Student') AND ([s0].[ID] = @__id_0)
ORDER BY [s0].[ID]
) AS [t] ON [s.Enrollments].[StudentID] = [t].[ID]
ORDER BY [t].[ID]

Observará algo que podría sorprenderle: el SQL selecciona hasta 2 filas ( TOP(2) ) de la tabla
Person. El método SingleOrDefaultAsync no se resuelve en 1 fila en el servidor. Este es el
por qué:

• Si la consulta devuelve varias filas, el método devuelve null.


• Para determinar si la consulta devolverá varias filas, EF tiene que comprobar si devuelve al
menos 2.

Tenga en cuenta que no tiene que utilizar el modo de depuración y detenerse en un punto
de interrupción para obtener salida de registro en la ventana de resultados . Es sólo una
forma conveniente de detener el registro en el punto en el que desea ver la salida. Si no lo
hace, el registro continuará y tendrá que desplazarse de nuevo para encontrar las partes
que le interesan.

Patrones de repositorio y unidad de trabajo

Muchos desarrolladores escriben código para implementar el repositorio y la unidad de


patrones de trabajo como un contenedor alrededor de código que funciona con Entity
Framework. Estos patrones están destinados a crear una capa de abstracción entre la capa
de acceso a datos y la capa de lógica de negocio de una aplicación. La implementación de
estos patrones puede ayudar a aislar su aplicación de los cambios en el almacén de datos y
puede facilitar la prueba de unidades automatizadas o el desarrollo impulsado por pruebas
(TDD). Sin embargo, escribir código adicional para implementar estos patrones no siempre
es la mejor opción para aplicaciones que utilizan EF, por varias razones:

• La clase de contexto EF aísla su código del código específico de almacén de datos.


• La clase de contexto EF puede actuar como una clase de unidad de trabajo para las
actualizaciones de la base de datos que realice con EF.
• EF incluye características para implementar TDD sin escribir código de repositorio.

Para obtener información acerca de cómo implementar el repositorio y la unidad de


patrones de trabajo, vea la versión de Entity Framework 5 de esta serie de tutoriales .

Entity Framework Core implementa un proveedor de base de datos en memoria que puede
utilizarse para realizar pruebas. Para obtener más información, consulte Pruebas con
InMemory .

Detección automática de cambios

El Entity Framework determina cómo una entidad ha cambiado (y por lo tanto, qué
actualizaciones deben ser enviadas a la base de datos) comparando los valores actuales de
una entidad con los valores originales. Los valores originales se almacenan cuando se
consulta o se adjunta la entidad. Algunos de los métodos que causan la detección
automática de cambios son los siguientes:

• DbContext.SaveChanges
• DbContext.Entry
• ChangeTracker.Entries

Si está rastreando un gran número de entidades y llama a uno de estos métodos muchas
veces en un bucle, es posible que obtenga mejoras significativas de rendimiento al
desactivar temporalmente la detección automática de cambios utilizando la
propiedad ChangeTracker.AutoDetectChangesEnabled . Por ejemplo:

_context.ChangeTracker.AutoDetectChangesEnabled = false;

Entity Framework Código fuente principal y planes de desarrollo

El código fuente de Entity Framework Core está disponible


en https://github.com/aspnet/EntityFrameworkCore . Además del código fuente, puede
obtener compilaciones todas las noches, seguimiento de emisiones, especificaciones de
características, notas de la reunión de diseño, la hoja de ruta para el desarrollo futuro y
más. Puede archivar bugs y puede aportar sus propias mejoras al código fuente de EF.

Aunque el código fuente está abierto, Entity Framework Core es totalmente compatible
como un producto de Microsoft. El equipo de Entity Framework de Microsoft mantiene el
control sobre qué contribuciones se aceptan y prueba todos los cambios de código para
garantizar la calidad de cada versión.

Ingenieria inversa de la base de datos existente

Para realizar un ingeniería inversa de un modelo de datos que incluya clases de entidad de
una base de datos existente, utilice el comando scaffold-dbcontext . Consulte el tutorial de
inicio .

Utilizar LINQ dinámico para simplificar el código de selección de clasificación

El tercer capítulo de esta tutorial muestra cómo escribir código LINQ mediante codificación
de nombres de columna en una instrucción switch . Con dos columnas para elegir, esto
funciona bien, pero si tiene muchas columnas el código podría ser complejo. Para resolver
ese problema, puede utilizar el método EF.Property para especificar el nombre de la
propiedad como una cadena. Para probar este enfoque, reemplace el método Index en
el StudentsController con el código siguiente.

public async Task<IActionResult> Index(


string sortOrder,
string currentFilter,
string searchString,
int? page)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] =
String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
ViewData["DateSortParm"] =
sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";

if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}

ViewData["CurrentFilter"] = searchString;

var students = from s in _context.Students


select s;

if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}

if (string.IsNullOrEmpty(sortOrder))
{
sortOrder = "LastName";
}

bool descending = false;


if (sortOrder.EndsWith("_desc"))
{
sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
descending = true;
}

if (descending)
{
students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
}
else
{
students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
}

int pageSize = 3;
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
page ?? 1, pageSize));
}

Próximos pasos

Esto completa esta serie de tutoriales sobre el uso de Entity Framework Core en una
aplicación ASP.NET MVC.

Para obtener más información acerca de EF Core, consulte la documentación de Entity


Framework Core . Un libro también está disponible: Entity Framework Core in Action .

Para obtener información sobre cómo implementar la aplicación web después de crearla,
consulte Publicación e implementación .

Para obtener información acerca de otros temas relacionados con ASP.NET Core MVC,
como autenticación y autorización, consulte la documentación básica de ASP.NET .

También podría gustarte