Tutorial: Implementar la funcionalidad CRUD con Entity Framework en ASP.NET MVC
En el tutorial anterior, creó una aplicación MVC que almacena y muestra los datos con Entity Framework (EF) 6 y SQL Server LocalDB. En este tutorial, podrá revisar y personalizar el código CRUD (crear, leer, actualizar y eliminar) que el scaffolding de MVC crea automáticamente para usted en controladores y vistas.
Nota:
Es una práctica habitual implementar el modelo de repositorio con el fin de crear una capa de abstracción entre el controlador y la capa de acceso a datos. Para que estos tutoriales sean sencillos y se centren en enseñar a usar EF 6, no se usan repositorios. Para obtener información sobre cómo implementar repositorios, consulte el Mapa de contenido de acceso a datos de ASP.NET.
Estos son ejemplos de las páginas web que crea:
En este tutorial ha:
- Creará una página de detalles
- Actualizar la página Create
- Actualizará el método HttpPost Edit
- Actualizar la página Delete
- Cerrar conexiones de bases de datos
- Controlar transacciones
Requisitos previos
Crear una página de detalles
En el código con scaffolding de la página de Index
de Students se excluyó la propiedad Enrollments
porque contiene una colección. En la página Details
, se mostrará el contenido de la colección en una tabla HTML.
En Controllers/StudentController.cs, el método de acción para la vista Details
usa el método Find para recuperar una única entidad Student
.
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
El valor de clave se pasa al método como parámetro id
y procede de los datos de ruta del hipervínculo de detalles en la página de índice.
Sugerencia: Datos de ruta
Los datos de ruta son datos que el enlazador de modelos encontró en un segmento de dirección URL especificado en la tabla de enrutamiento. Por ejemplo, la ruta predeterminada especifica los segmentos de controller
, action
e id
:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
En la dirección URL siguiente, la ruta predeterminada asigna Instructor
como el controller
, Index
como la action
y 1 como el id
; estos son los valores de datos de ruta.
http://localhost:1230/Instructor/Index/1?courseID=2021
?courseID=2021
es un valor de cadena de consulta. El enlazador de modelos también funcionará si pasa el id
como valor de cadena de consulta:
http://localhost:1230/Instructor/Index?id=1&CourseID=2021
Las instrucciones ActionLink
crean las direcciones URL en la vista de Razor. En el siguiente código, el parámetro id
coincide con la ruta predeterminada, por lo que se agrega id
a los datos de la ruta.
@Html.ActionLink("Select", "Index", new { id = item.PersonID })
En el siguiente código, courseID
, no coincide con ningún parámetro de la ruta predeterminada, por lo que se agrega como una cadena de consulta.
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
Para crear la página de detalles
Abra Views/Students/Details.cshtml.
Cada campo se muestra mediante un asistente
DisplayFor
, como se muestra en el ejemplo siguiente:<dt> @Html.DisplayNameFor(model => model.LastName) </dt> <dd> @Html.DisplayFor(model => model.LastName) </dd>
Después del campo
EnrollmentDate
e inmediatamente antes de la etiqueta</dl>
de cierre, agregue el código resaltado para mostrar una lista de inscripciones, como se muestra en el ejemplo siguiente:<dt> @Html.DisplayNameFor(model => model.EnrollmentDate) </dt> <dd> @Html.DisplayFor(model => model.EnrollmentDate) </dd> <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> </dl> </div> <p> @Html.ActionLink("Edit", "Edit", new { id = Model.ID }) | @Html.ActionLink("Back to List", "Index") </p>
Si la sangría de código no es correcta después de pegar el código, presione Ctrl+K, Ctrl+D para formatearlo.
Este código recorre en bucle las entidades en la propiedad de navegación
Enrollments
. Para cada entidadEnrollment
de la propiedad, se muestra el título del curso y la calificación. El título del curso se recupera de la entidadCourse
almacenada en la propiedad de navegaciónCourse
de la entidadEnrollments
. Todos estos datos se recuperan de la base de datos automáticamente cuando es necesario. Dicho de otra forma, aquí usa la carga diferida. No especificó la carga diligente para la propiedad de navegaciónCourses
, por lo que las inscripciones no se recuperaron en la misma consulta que obtuvo los alumnos. En su lugar, la primera vez que intenta acceder a la propiedad de navegaciónEnrollments
, se envía una nueva consulta a la base de datos para recuperar los datos. Puede leer más información sobre la carga diferida y la carga diligente en el tutorial Lectura de datos relacionados más adelante en esta serie.Para abrir la página de detalles, inicie el programa (Ctrl+F5), seleccione la pestaña Alumnos y, a continuación, haga clic en el vínculo de detalles de Alexander Carson (si presiona Ctrl+F5 mientras el archivo Details.cshtml está abierto, obtendrá un error HTTP 400. Esto se debe a que Visual Studio intenta ejecutar la página de detalles, pero sin que se haya accedido a ella desde un vínculo que especifica el alumno que se va a mostrar. Si esto sucede, quite "Student/Details" de la dirección URL e inténtelo de nuevo, o cierre el explorador, haga clic con el botón derecho en el proyecto y haga clic en Ver>Ver en el explorador).
Verá la lista de cursos y calificaciones para el alumno seleccionado.
Cierre el explorador.
Actualizar la página Create
En Controllers\StudentController.cs, reemplace el método de acción HttpPostAttribute
Create
por el código siguiente. Este código agrega un bloquetry-catch
y quitaID
del atributo BindAttribute del método con scaffolding:[HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "LastName, FirstMidName, EnrollmentDate")]Student student) { try { if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); } } catch (DataException /* dex */) { //Log the error (uncomment dex variable name and add a line here to 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
Student
creada por el enlazador de modelos MVC de ASP.NET al conjunto de entidadesStudents
y después guarda los cambios en la base de datos. El enlazador de modelos se refiere a la funcionalidad de ASP.NET MVC que facilita trabajar con datos enviados por un formulario; un enlazador de modelos convierte los valores de formulario enviados en tipos CLR y los pasa al método de acción en parámetros. En este caso, el enlazador de modelos crea instancias de una entidadStudent
mediante valores de propiedad de la colecciónForm
.Se ha quitado
ID
del atributo Bind porqueID
es el valor de clave principal que SQL Server establecerá automáticamente cuando se inserte la fila. La entrada del usuario no establece el valorID
.Advertencia de seguridad: el atributo
ValidateAntiForgeryToken
ayuda a evitar ataques de falsificación de solicitud entre sitios. Requiere una instrucciónHtml.AntiForgeryToken()
correspondiente en la vista, que verá más adelante.El atributo
Bind
es una manera de proteger contra el exceso de publicación en escenarios de creación. Por ejemplo, suponga que la entidadStudent
incluye una propiedadSecret
que no quiere que esta página web establezca.public class Student { public int ID { get; set; } public string LastName { get; set; } public string FirstMidName { get; set; } public DateTime EnrollmentDate { get; set; } public string Secret { get; set; } public virtual ICollection<Enrollment> Enrollments { get; set; } }
Aunque no tenga un campo
Secret
en la página web, un hacker podría usar una herramienta como fiddler, o bien escribir código de JavaScript, para enviar un valor de formularioSecret
. Sin el atributo BindAttribute para limitar los campos que el enlazador de modelos usa cuando crea una instanciaStudent
, el enlazador de modelos seleccionaría ese valor de formularioSecret
y lo usaría para crear la instancia de la entidadStudent
. Después, el valor que el hacker haya especificado para el campo de formularioSecret
se actualizaría en la base de datos. En la imagen siguiente se muestra cómo la herramienta fiddler agrega el campoSecret
(con el valor "OverPost") a los valores de formulario publicados.Después, el valor "OverPost" se agregaría correctamente a la propiedad
Secret
de la fila insertada, aunque no hubiera previsto que la página web pudiera establecer esa propiedad.Es mejor usar el parámetro
Include
con el atributoBind
para enumerar explícitamente los campos. También es posible usar el parámetroExclude
para bloquear los campos que desea excluir. La razónInclude
más segura es que al agregar una nueva propiedad a la entidad, una listaExclude
no protege el nuevo campo automáticamente.Puede evitar la publicación excesiva en escenarios de edición si primero lee la entidad desde la base de datos y después llama a
TryUpdateModel
, pasando una lista de propiedades permitidas de manera explícita. Es el método que se usa en estos tutoriales.Una manera alternativa de evitar la publicación excesiva que muchos desarrolladores prefieren consiste en usar modelos de vista en lugar de clases de entidad con el enlace de modelos. Incluya en el modelo de vista solo las propiedades que quiera actualizar. Una vez que haya finalizado el enlazador de modelos de MVC, copie las propiedades del modelo de vista a la instancia de entidad, opcionalmente con una herramienta como AutoMapper. Use db.Entry en la instancia de entidad para establecer su estado en Inalterada y, después, establezca Property("PropertyName").IsModified en true en todas las propiedades de entidad que se incluyan en el modelo de vista. Este método funciona tanto en escenarios de edición como de creación.
Aparte del atributo
Bind
, el bloquetry-catch
es el único cambio que se ha realizado en el código con scaffolding. Si se detecta una excepción derivada de DataException mientras se guardan los cambios, se muestra un mensaje de error genérico. En ocasiones, las excepciones DataException se deben a algo externo a la aplicación y no a un error de programación, por lo que se recomienda al usuario que vuelva a intentarlo. Aunque no se ha implementado en este ejemplo, en una aplicación de producción de calidad se debería registrar la excepción. Para obtener más información, vea la sección Registro para obtener información de Supervisión y telemetría (creación de aplicaciones de nube reales con Azure).El código de Views\Student\Create.cshtml es similar al que vio en Details.cshtml, excepto que los asistentes
EditorFor
yValidationMessageFor
se usan para cada campo en lugar deDisplayFor
. Este es el código pertinente:<div class="form-group"> @Html.LabelFor(model => model.LastName, new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.LastName) @Html.ValidationMessageFor(model => model.LastName) </div> </div>
Create.cshtml también incluye
@Html.AntiForgeryToken()
, que funciona con el atributoValidateAntiForgeryToken
en el controlador para contribuir a evitar ataques de falsificación de solicitud entre sitios.No es preciso realizar cambios en Create.cshtml.
Para ejecutar la página, inicie el programa, seleccione la pestaña Alumnos y, a continuación, haga clic en Crear nuevo.
Escriba los nombres y una fecha no válida y haga clic en Crear para ver el mensaje de error.
Es la validación del lado servidor que obtendrá de forma predeterminada. En un tutorial posterior, verá cómo agregar atributos que generan código para la validación del lado cliente. En el siguiente código resaltado se muestra la comprobación de validación del modelo en el método Create.
if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); }
Cambie la fecha por un valor válido y haga clic en Crear para ver el alumno nuevo en la página Index.
Cierre el explorador.
Actualizar el método HttpPost Edit
Reemplace el método de acción HttpPostAttribute
Edit
por el código siguiente:[HttpPost, ActionName("Edit")] [ValidateAntiForgeryToken] public ActionResult EditPost(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } var studentToUpdate = db.Students.Find(id); if (TryUpdateModel(studentToUpdate, "", new string[] { "LastName", "FirstMidName", "EnrollmentDate" })) { try { db.SaveChanges(); return RedirectToAction("Index"); } catch (DataException /* dex */) { //Log the error (uncomment dex variable name and add a line here to write a log. ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator."); } } return View(studentToUpdate); }
Nota:
En Controllers\StudentController.cs, el método
HttpGet Edit
(el que no tiene el atributoHttpPost
) usa el métodoFind
para recuperar la entidadStudent
seleccionada, como se vio en el métodoDetails
. No es necesario cambiar este método.Estos cambios implementan un procedimiento recomendado de seguridad para evitar la publicación excesiva. El proveedor de scaffolding generó un atributo
Bind
y agregó la entidad creada por el enlazador de modelos a la entidad establecida con una marca modificada. Ese código ya no se recomienda porque el atributoBind
borra los datos ya existentes en los campos que no se enumeran en el parámetroInclude
. En el futuro, el proveedor de scaffolding del controlador MVC se actualizará para que no genere atributosBind
de los métodos Edit.El código nuevo lee la entidad existente y llama a TryUpdateModel para actualizar los campos de entrada de usuario en los datos de formulario publicados. El seguimiento de cambios automático de Entity Framework establece la marca EntityState.Modified en la entidad. Cuando se llama al método SaveChanges, la marca Modified hace que Entity Framework cree instrucciones SQL para actualizar la fila de la base de datos. Los conflictos de simultaneidad se ignoran y todas las columnas de la fila de la base de datos se actualizan, incluidas aquellas que el usuario no cambió (en un tutorial posterior se muestra cómo controlar los conflictos de simultaneidad y, si solo desea actualizar campos individuales en la base de datos, puede establecer la entidad en EntityState.Unchanged y establecer los campos individuales en EntityState.Modified).
Para evitar la publicación excesiva, los campos que quiera que se puedan actualizar por la página de edición se enumeran en los parámetros
TryUpdateModel
. Actualmente no se está protegiendo ningún campo adicional, pero enumerar los campos que quiere que el enlazador de modelos enlace garantiza que, si en el futuro agrega campos al modelo de datos, se protejan automáticamente hasta que los agregue aquí de forma explícita.Como resultado de estos cambios, la firma de método del método HttpPost Edit es la misma que la del método HttpGet Edit; por tanto, se ha cambiado el nombre del método EditPost.
Sugerencia
Estados de entidad y los métodos Attach y SaveChanges
El contexto de la base de datos realiza el seguimiento de si las entidades en memoria están sincronizadas con sus filas correspondientes en la base de datos, y esta información determina lo que ocurre cuando se llama al método
SaveChanges
. Por ejemplo, cuando se pasa una nueva entidad al método Add, el estado de esa entidad se establece enAdded
. Después, cuando se llama al método SaveChanges, el contexto de la base de datos emite un comandoINSERT
de SQL.Una entidad puede estar en uno de los estados siguientes:
Added
. La entidad no existe todavía en la base de datos. El métodoSaveChanges
debe emitir una instrucciónINSERT
.Unchanged
. No es necesario hacer nada con esta entidad mediante el métodoSaveChanges
. Al leer una entidad de la base de datos, la entidad empieza con este estado.Modified
. Se han modificado algunos o todos los valores de propiedad de la entidad. El métodoSaveChanges
debe emitir una instrucciónUPDATE
.Deleted
. La entidad se ha marcado para su eliminación. El métodoSaveChanges
debe emitir una instrucciónDELETE
.Detached
. El contexto de base de datos no está realizando el seguimiento de la entidad.
En una aplicación de escritorio, los cambios de estado normalmente se establecen de forma automática. En un tipo de aplicación de escritorio, se lee una entidad y se realizan cambios en algunos de sus valores de propiedad. Esto hace que su estado de entidad cambie automáticamente a
Modified
. Después, cuando se llama aSaveChanges
, Entity Framework genera una instrucciónUPDATE
de SQL que solo actualiza las propiedades reales que se hayan cambiado.La naturaleza desconectada de las aplicaciones web no permite esta secuencia continua. La instancia de DbContext que lee una entidad se elimina después de representar una página. Cuando se llama al método de acción
HttpPost
Edit
, se realiza una nueva solicitud y se tiene una nueva instancia de DbContext, por lo que debe establecer manualmente el estado de la entidad enModified.
. Después, al llamar aSaveChanges
, Entity Framework actualiza todas las columnas de la fila de base de datos, ya que el contexto no tiene ninguna manera de saber qué propiedades se han cambiado.Si quiere que la instrucción
Update
de SQL actualice solo los campos que el usuario ha cambiado realmente, puede guardar los valores originales de alguna manera (como campos ocultos) para que estén disponibles cuando se llame al métodoHttpPost
Edit
. Después puede crear una entidadStudent
con los valores originales, llamar al métodoAttach
con esa versión original de la entidad, actualizar los valores de la entidad con los valores nuevos y luego llamar aSaveChanges.
. Para obtener más información, consulte Estados de entidad y SaveChanges y Datos locales.El código HTML y de Razor de Views\Student\Edit.cshtml es similar a lo que vio en Create.cshtml y no es necesario ningún cambio.
Para ejecutar la página, inicie el programa, seleccione la pestaña Alumnos y, a continuación, haga clic en un hipervínculo de edición.
Cambie algunos de los datos y haga clic en Guardar. Verá los datos modificados en la página de índice.
Cierre el explorador.
Actualizar la página Delete
En Controllers\StudentController.cs, el código de plantilla del método HttpGetAttribute Delete
usa el método Find
para recuperar la entidad Student
seleccionada, como se ha visto en los métodos Details
y Edit
. Pero para implementar un mensaje de error personalizado cuando se produce un error en la llamada a SaveChanges
, agregará funcionalidad a este método y su vista correspondiente.
Como se vio para las operaciones de actualización y creación, las operaciones de eliminación requieren dos métodos de acción. El método que se llama en respuesta a una solicitud GET muestra una vista que proporciona al usuario la oportunidad de aprobar o cancelar la operación de eliminación. Si el usuario la aprueba, se crea una solicitud POST. Cuando esto ocurre, se llama al método HttpPost
Delete
y, después, ese método es el que realiza la operación de eliminación.
Agregará un bloque try-catch
al método HttpPostAttribute Delete
para controlar los errores que se puedan producir cuando se actualice la base de datos. Si se produce un error, el método HttpPostAttribute Delete
llama al método HttpGetAttribute Delete
, y pasa un parámetro que indica que se ha producido un error. Después, el método HttpGetAttribute Delete
vuelve a mostrar la página de confirmación junto con el mensaje de error, lo que da al usuario la oportunidad de cancelar la acción o volver a intentarlo.
Reemplace el método de acción HttpGetAttribute
Delete
con el código siguiente, que administra los informes de errores:public ActionResult Delete(int? id, bool? saveChangesError=false) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } if (saveChangesError.GetValueOrDefault()) { ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator."; } Student student = db.Students.Find(id); if (student == null) { return HttpNotFound(); } return View(student); }
Este código acepta un parámetro opcional que indica si se llamó al método después de un error al guardar los cambios. Este parámetro es
false
cuando se llama al métodoHttpGet
Delete
sin un error anterior. Cuando se llama por medio del métodoHttpPost
Delete
en respuesta a un error de actualización de base de datos, el parámetro estrue
y se pasa un mensaje de error a la vista.Reemplace el método de acción HttpPostAttribute
Delete
(denominadoDeleteConfirmed
) por el código siguiente, que realiza la operación de eliminación real y captura los errores de actualización de la base de datos.[HttpPost] [ValidateAntiForgeryToken] public ActionResult Delete(int id) { try { Student student = db.Students.Find(id); db.Students.Remove(student); db.SaveChanges(); } catch (DataException/* dex */) { //Log the error (uncomment dex variable name and add a line here to write a log. return RedirectToAction("Delete", new { id = id, saveChangesError = true }); } return RedirectToAction("Index"); }
Este código recupera la entidad seleccionada y después llama al método Remove para establecer el estado de la entidad en
Deleted
. Cuando se llama aSaveChanges
, se genera un comandoDELETE
de SQL. También ha cambiado el nombre del método de acción deDeleteConfirmed
aDelete
. El código al que se aplicó la técnica scaffolding ha asignado al métodoDelete
deHttpPost
el nombreDeleteConfirmed
para proporcionar al métodoHttpPost
una firma única. (El CLR requiere métodos sobrecargados para tener parámetros de método diferentes). Ahora que las firmas son únicas, puede ceñirse a la convención MVC y usar el mismo nombre para los métodos de eliminaciónHttpPost
yHttpGet
.Si mejorar el rendimiento de una aplicación de gran volumen es una prioridad, podría evitar una consulta SQL innecesaria para recuperar la fila mediante el reemplazo de las líneas de código que llaman a los métodos
Find
yRemove
por el código siguiente:Student studentToDelete = new Student() { ID = id }; db.Entry(studentToDelete).State = EntityState.Deleted;
Este código crea una instancia de una entidad
Student
solo con el valor de clave principal y después estableciendo el estado de la entidad enDeleted
. Eso es todo lo que necesita Entity Framework para eliminar la entidad.Como se ha indicado, el método
HttpGet
Delete
no elimina los datos. Realizar una operación de eliminación en respuesta a una solicitud GET (o con este propósito, efectuar una operación de edición, creación o cualquier otra operación que modifique los datos) presenta un riesgo de seguridad.En Views/Student/Delete.cshtml, agregue un mensaje de error entre los títulos
h2
yh3
, como se muestra en el ejemplo siguiente:<h2>Delete</h2> <p class="error">@ViewBag.ErrorMessage</p> <h3>Are you sure you want to delete this?</h3>
Para ejecutar la página, inicie el programa, seleccione la pestaña Alumnos y, a continuación, haga clic en un hipervínculo de eliminación.
Elija Eliminar en la página en la que se indica ¿Seguro que quiere eliminarlo?
Se mostrará la página de índice sin el alumno eliminado (verá un ejemplo del código de control de errores en funcionamiento en el tutorial sobre la simultaneidad).
Cerrar conexiones de bases de datos
Para cerrar conexiones de bases de datos y liberar los recursos que contienen lo antes posible, elimine la instancia de contexto cuando haya terminado. Ese es el motivo por el que el código con scaffolding proporciona un método Dispose al final de la clase StudentController
en StudentController.cs, como se muestra en el ejemplo siguiente:
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
La clase de Controller
base ya implementa la interfaz IDisposable
, por lo que este código simplemente agrega una invalidación al método Dispose(bool)
para eliminar explícitamente la instancia de contexto.
Controlar transacciones
De forma predeterminada, Entity Framework implementa las transacciones de manera implícita. En escenarios donde se realizan cambios en varias filas o tablas, y después se llama a SaveChanges
, Entity Framework se asegura automáticamente de que todos los cambios se realicen correctamente o se produzca un error en todos ellos. Si primero se realizan algunos cambios y después se produce un error, los cambios se revierten automáticamente. Para escenarios donde se necesita más control, por ejemplo, si se quiere incluir operaciones realizadas fuera de Entity Framework en una transacción, consulte Trabajar con transacciones.
Obtención del código
Descargar el proyecto completado
Recursos adicionales
Ahora tiene un conjunto completo de páginas que realizan sencillas operaciones CRUD para entidades Student
. Ha usado asistentes MVC para generar elementos de interfaz de usuario para campos de datos. Para obtener más información sobre los asistentes MVC, consulte Representar un formulario mediante asistentes de HTML (el artículo es para MVC 3, pero sigue siendo pertinente para MVC 5).
Encontrará vínculos a otros recursos de EF 6 en Acceso a datos de ASP.NET: Recursos recomendados.
Pasos siguientes
En este tutorial ha:
- Se ha creado una página de detalles
- Actualizado la página Create
- Se ha actualizado el método HttpPost Edit
- Actualizado la página Delete
- Cerrado conexiones de bases de datos
- Se han controlado las transacciones
Pase al siguiente artículo para aprender a agregar ordenación, filtrado y paginación al proyecto.