Tutorial - 2. Introducción A Core y Entity Framework Core de ASP
Tutorial - 2. Introducción A Core y Entity Framework Core de ASP
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:
Abra Visual Studio y cree un nuevo proyecto web ASP.NET Core C # denominado
"ContosoUniversity".
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.
Algunos cambios sencillos configurarán el menú del sitio, el diseño y la página de inicio.
<!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>
<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>
@{
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 »</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 »</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
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
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; }
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
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
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.
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 ).
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; }
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
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)
{
}
}
}
}
Registrar el contexto con la inyección de dependencia
Para registrar SchoolContext como un servicio, abra Startup.cs y agregue las líneas resaltadas
al método ConfigureServices .
services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddMvc();
}
using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
"ConnectionStrings": {
"DefaultConnection":
"Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity1;Trusted_Connection=True;Multipl
eActiveResultSets=true"
},
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
}
}
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> .
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.
using ContosoUniversity.Models;
using System;
using System.Linq;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
context.Database.EnsureCreated();
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.
host.Run();
}
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Data;
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
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 .
• 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.
namespace ContosoUniversity.Controllers
{
public class StudentsController : Controller
{
private readonly SchoolContext _context;
{
_context = context;
}
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:
Aprenderá sobre los elementos de programación asíncronos en este código más adelante
en el tutorial.
@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
Cierre el navegador.
Si la ventana SSOX no está abierta, selecciónela desde el menú Ver en Visual Studio.
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> .
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 ).
• 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.
if (id == null)
return NotFound();
if (student == null)
{
return NotFound();
}
return View(student);
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?}");
});
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
<a href="/Students/Edit/6">Edit</a>
<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.
[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 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.
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.
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 .
[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 .
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).
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.
Estados de Entidad
• 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 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 .
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
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.
{
if (id == 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.
[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.
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
{
_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.
<h2>Delete</h2>
<p class="text-danger">@ViewData["ErrorMessage"]</p>
<h3>Are you sure you want to delete this?</h3>
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.
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.
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.
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.
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 .
@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>
{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
ViewData["CurrentFilter"] = searchString;
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());
}
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.
<p>
<a asp-action="Create">Create New</a>
</p>
<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.
http://localhost:5813/Students?SearchString=an
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; }
this.AddRange(items);
}
{
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;
int pageSize = 3;
if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}
@model PaginatedList<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"]"
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["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.
using System;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }
using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;
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.
@model IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>
@{
ViewData["Title"] = "Student Body Statistics";
}
<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>
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.
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.
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 .
<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.)
{
"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 :
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:
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
<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.
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.
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");
});
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);
});
}
}
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 .
En la ventana de comandos, ingrese el siguiente comando para crear la base de datos y las
tablas en ella.
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])
);
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.
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 .
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; }
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.
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; }
[RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
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.
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.
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; }
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
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; }
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.
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
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; }
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:
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.
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).
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; }
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.
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; }
[Range(0, 5)]
public int Credits { 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
[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.
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.
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:
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 ):
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; }
[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; }
[Column(TypeName="money")]
public decimal Budget { get; set; }
Un departamento puede tener muchos cursos, por lo que hay una propiedad de navegación
de Cursos:
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)
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
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 :
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
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; }
}
}
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 .
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
modelBuilder.Entity<Department>().ToTable("Department");
modelBuilder.Entity<Instructor>().ToTable("Instructor");
modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
modelBuilder.Entity<CourseAssignment>()
Este código agrega las nuevas entidades y configura la clave primaria compuesta de la
entidad CourseAssignment.
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.
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.
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();
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:
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);
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"
},
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 :
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.
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 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.
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 ):
@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>
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.
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.
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
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.
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 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.
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.
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:
En lugar de:
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
@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)
{
<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>
• 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.
</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.
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
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.
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 = 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:
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.
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.
Para optimizar el rendimiento de los detalles del curso y eliminar páginas, agregue
llamadas AsNoTracking en los métodos HttpGet Details y Delete .
return View(course);
}
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
return View(course);
}
<div class="form-group">
En Views / Courses / Edit.cshtml , realice el mismo cambio para el campo Departamento que
acaba de hacer en Create.cshtml .
<div class="form-group">
<label asp-for="CourseID" class="control-label"></label>
<div>@Html.DisplayFor(model => model.CourseID)</div>
</div>
@model ContosoUniversity.Models.Course
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<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>
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.
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:
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();
}
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);
}
• 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))
<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>
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.
Para proporcionar datos a la vista de la lista de casillas de verificación, utilizará una clase de
modelo de la vista.
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; }
}
}
{
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 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 .
[HttpPost]
[ValidateAntiForgeryToken]
{
if (id == null)
{
return NotFound();
}
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);
}
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.
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
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 .
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);
}
}
}
}
else
{
if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove =
instructorToUpdate.CourseAssignments.SingleOrDefault(i => i.CourseID == course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}
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;
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
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
_context.Instructors.Remove(instructor);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
PopulateAssignedCourseData(instructor);
return View();
}
// POST: Instructors/Create
[HttpPost]
[ValidateAntiForgeryToken]
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).
<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;
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
Concurrencia optimista
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.
• 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.
[! 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; }
[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; }
#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; }
[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; }
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:
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:
El motor de andamios creó una columna RowVersion en la vista de índice, pero ese campo
no debe mostrarse.
@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>
Reemplace el código existente para el método HttpPost Edit con el código siguiente:
[HttpPost]
[ValidateAntiForgeryToken]
if (id == null)
return NotFound();
if (departmentToUpdate == null)
await TryUpdateModelAsync(deletedDepartment);
ModelState.AddModelError(string.Empty,
return View(deletedDepartment);
_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue =
rowVersion;
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"",
try
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
if (databaseEntry == null)
ModelState.AddModelError(string.Empty,
else
{
var databaseValues = (Department)databaseEntry.ToObject();
if (databaseValues.Name != clientValues.Name)
if (databaseValues.Budget != clientValues.Budget)
if (databaseValues.StartDate != clientValues.StartDate)
if (databaseValues.InstructorID != clientValues.InstructorID)
ModelState.Remove("RowVersion");
return View(departmentToUpdate);
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.
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.
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;
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).
if (databaseValues.Name != clientValues.Name)
departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");
@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>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
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.
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.
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);
}
[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 });
}
}
@model ContosoUniversity.Models.Department
@{
ViewData["Title"] = "Delete";
<h2>Delete</h2>
<p class="text-danger">@ViewData["ConcurrencyErrorMessage"]</p>
<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>
@model ContosoUniversity.Models.Department
@{
ViewData["Title"] = "Details";
<h2>Details</h2>
<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
</dt>
<dd>
</dd>
<dt>
</dt>
<dd>
</dd>
<dt>
</dt>
<dd>
</dd>
<dt>
</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>
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.
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; }
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; }
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; }
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
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.
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:
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).
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 .
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.
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:
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.
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:
if (department == null)
{
return NotFound();
}
return View(department);
}
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);
}
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:
[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();
}
@{
ViewBag.Title = "UpdateCourseCredits";
}
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.
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.
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é:
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.
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 .
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;
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.
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 .
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.
if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}
ViewData["CurrentFilter"] = searchString;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
if (string.IsNullOrEmpty(sortOrder))
{
sortOrder = "LastName";
}
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 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 .