Implementación de la funcionalidad CRUD básica con Entity Framework en ASP.NET MVC (2 de 10)
por Tom Dykstra
En la aplicación web de ejemplo Contoso University, se muestra cómo crear aplicaciones ASP.NET MVC 4 con Code First de Entity Framework 5 y Visual Studio 2012. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial de la serie.
Nota:
Si se encontrase problemas que no pudiera resolver, descargue el capítulo completo e intente reproducir el problema. Por lo general, es posible encontrar la solución al problema comparando el propio código con el código completado. Para conocer algunos errores comunes y cómo resolverlos, consulte Errores y soluciones alternativas.
En el tutorial anterior, creó una aplicación MVC que almacena y muestra datos con Entity Framework y SQL Server LocalDB. En este tutorial, revisará y personalizará el código CRUD (crear, leer, actualizar y eliminar) que se crea automáticamente con el andamiaje de MVC 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 simplificar estos tutoriales, no se implementará un repositorio hasta un tutorial posterior de esta serie.
En este tutorial, se crearán las páginas web siguientes:
Creación de una página Details
En el código con andamiaje de la página 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 = 0)
{
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
El valor de la clave se pasa al método en forma del parámetro id
y procede de los datos de ruta del hipervínculo Details de la página Index.
Abra Views/Students/Details.cshtml. Cada campo se muestra mediante un asistente
DisplayFor
, como se ilustra en el ejemplo siguiente:<div class="display-label"> @Html.DisplayNameFor(model => model.LastName) </div> <div class="display-field"> @Html.DisplayFor(model => model.LastName) </div>
Después del campo
EnrollmentDate
e inmediatamente antes de la etiquetafieldset
de cierre, agregue código para mostrar una lista de inscripciones, como se ilustra en el ejemplo siguiente:<div class="display-label"> @Html.LabelFor(model => model.Enrollments) </div> <div class="display-field"> <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> </div> </fieldset> <p> @Html.ActionLink("Edit", "Edit", new { id=Model.StudentID }) | @Html.ActionLink("Back to List", "Index") </p>
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í se usa la carga diferida. Como no se especificó carga diligente para la propiedad de navegaciónCourses
, la primera vez que intente acceder a esa propiedad, se enviará una 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).Ejecute la página seleccionando la pestaña Students y haciendo clic en un vínculo Details para Alexander Carson. Verá la lista de cursos y calificaciones para el alumno seleccionado:
Actualización de la página Create
En Controllers\StudentController.cs, reemplace el método de acción
HttpPost``Create
por el código siguiente para agregar un bloquetry-catch
y el atributo Bind al método con andamiaje:[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 after DataException 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 de ASP.NET MVC al conjunto de entidadesStudents
y después guarda los cambios en la base de datos. (El enlazador de modelos hace referencia 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
).El atributo
ValidateAntiForgeryToken
ayuda a evitar ataques de falsificación de solicitud entre sitios.
> [!WARNING]
> Security - The `Bind` attribute is added to protect against *over-posting*. For example, suppose the `Student` entity includes a `Secret` property that you don't want this web page to update.
>
> [!code-csharp[Main](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/samples/sample5.cs?highlight=7)]
>
> Even if you don't have a `Secret` field on the web page, a hacker could use a tool such as [fiddler](http://fiddler2.com/home), or write some JavaScript, to post a `Secret` form value. Without the [Bind](https://msdn.microsoft.com/library/system.web.mvc.bindattribute(v=vs.108).aspx) attribute limiting the fields that the model binder uses when it creates a `Student` instance*,* the model binder would pick up that `Secret` form value and use it to update the `Student` entity instance. Then whatever value the hacker specified for the `Secret` form field would be updated in your database. The following image shows the fiddler tool adding the `Secret` field (with the value "OverPost") to the posted form values.
>
> ![](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/_static/image6.png)
>
> The value "OverPost" would then be successfully added to the `Secret` property of the inserted row, although you never intended that the web page be able to update that property.
>
> It's a security best practice to use the `Include` parameter with the `Bind` attribute to *allowed attributes* fields. It's also possible to use the `Exclude` parameter to *blocked attributes* fields you want to exclude. The reason `Include` is more secure is that when you add a new property to the entity, the new field is not automatically protected by an `Exclude` list.
>
> Another alternative approach, and one preferred by many, is to use only view models with model binding. The view model contains only the properties you want to bind. Once the MVC model binder has finished, you copy the view model properties to the entity instance.
Other than the `Bind` attribute, the `try-catch` block is the only change you've made to the scaffolded code. If an exception that derives from [DataException](https://msdn.microsoft.com/library/system.data.dataexception.aspx) is caught while the changes are being saved, a generic error message is displayed. [DataException](https://msdn.microsoft.com/library/system.data.dataexception.aspx) exceptions are sometimes caused by something external to the application rather than a programming error, so the user is advised to try again. Although not implemented in this sample, a production quality application would log the exception (and non-null inner exceptions ) with a logging mechanism such as [ELMAH](https://code.google.com/p/elmah/).
The code in *Views\Student\Create.cshtml* is similar to what you saw in *Details.cshtml*, except that `EditorFor` and `ValidationMessageFor` helpers are used for each field instead of `DisplayFor`. The following example shows the relevant code:
[!code-cshtml[Main](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/samples/sample6.cshtml)]
*Create.cshtml* also includes `@Html.AntiForgeryToken()`, which works with the `ValidateAntiForgeryToken` attribute in the controller to help prevent [cross-site request forgery](../../security/xsrfcsrf-prevention-in-aspnet-mvc-and-web-pages.md) attacks.
No changes are required in *Create.cshtml*.
Para ejecutar la página, seleccione la pestaña Students y, luego, haga clic en Crear nuevo.
Algunas validaciones de datos funcionan de forma predeterminada. Escriba los nombres y una fecha no válida y haga clic en Crear para ver el mensaje de error.
En el siguiente código resaltado se muestra la comprobación de validación del modelo.
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Student student) { if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); } return View(student); }
Cambie la fecha por un valor válido, como 1/9/2005, y haga clic en Crear para ver el alumno nuevo en la página Index.
Actualización de la página Edit POST
En Controllers\StudentController.cs, el método HttpGet
Edit
(el que no tiene el atributo HttpPost
) usa el método Find
para recuperar la entidad Student
seleccionada, como se vio en el método Details
. No es necesario cambiar este método.
Sin embargo, reemplace el método de acción HttpPost
Edit
por el código siguiente para agregar un bloque try-catch
y el atributo Bind:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
[Bind(Include = "StudentID, LastName, FirstMidName, EnrollmentDate")]
Student student)
{
try
{
if (ModelState.IsValid)
{
db.Entry(student).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name after DataException 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 es similar al que vio en el método HttpPost
Create
. Sin embargo, en lugar de agregar la entidad creada por el enlazador de modelos al conjunto de entidades, este código establece una marca en la entidad que indica que se ha cambiado. Cuando se llama al método SaveChanges, la marca Modified hace que Entity Framework cree instrucciones SQL para actualizar la fila de base de datos. Todas las columnas de la fila de base de datos se actualizarán, incluidas las que el usuario no cambió, y se omiten los conflictos de simultaneidad. (En un tutorial posterior de esta serie, aprenderá a controlar la simultaneidad).
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 en Added
. Después, cuando se llama al método SaveChanges, el contexto de la base de datos emite un comando INSERT
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 a SaveChanges
, Entity Framework genera una instrucción UPDATE
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 en Modified.
. Después, al llamar a SaveChanges
, 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étodo HttpPost
Edit
. Después, puede crear una entidad Student
con los valores originales, llamar al método Attach
con esa versión original de la entidad, actualizar los valores de la entidad con los valores nuevos y luego llamar a SaveChanges.
. Para más información, consulte Estados de entidad y SaveChanges y Datos locales en MSDN Data Developer Center.
El código de Views\Student\Edit.cshtml es similar a lo que vio en Create.cshtml y no se requiere ningún cambio.
Para ejecutar la página, seleccione la pestaña Students y, luego, haga clic en un hipervínculo Editar.
Cambie algunos de los datos y haga clic en Guardar. Verá los datos modificados en la página Index.
Actualización de la página Delete
En Controllers\StudentController.cs, el código de plantilla del método HttpGet
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 HttpPost
Delete
para controlar los errores que se puedan producir cuando se actualice la base de datos. Si se produce un error, el método HttpPost
Delete
llama al método HttpGet
Delete
, y pasa un parámetro que indica que se ha producido un error. Después, el método HttpGet 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
HttpGet
Delete
con el código siguiente, que administra los informes de errores:public ActionResult Delete(bool? saveChangesError=false, int id = 0) { 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 booleano opcional que indica si se le llamó después de un error para 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
HttpPost
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 */) { // uncomment dex and log error. 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 reemplazando las líneas de código que llaman a los métodos
Find
yRemove
por el código siguiente, como se muestra en amarillo:Student studentToDelete = new Student() { StudentID = 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 establece 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. Para más información, consulte Sugerencia de ASP.NET MVC n.º 46: No usar Eliminar vínculos porque crean vulnerabilidades de seguridad en el blog de Stephen Walther.En Views/Student/Delete.cshtml, agregue un mensaje de error entre los encabezados
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, seleccione la pestaña Students y, luego, haga clic en el hipervínculo Eliminar:
Haga clic en Eliminar. Se mostrará la página de índice sin el estudiante eliminado. (Verá un ejemplo del código de control de errores en funcionamiento en el tutorial sobre control de la simultaneidad más adelante en esta serie).
Asegurarse de que las conexiones de base de datos no se dejan abiertas
Para asegurarse de que las conexiones de base de datos están cerradas correctamente y que los recursos que ocupan se liberan, debe ver que la instancia de contexto se elimine. Ese es el motivo de el que el código con andamiaje proporcione 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)
{
db.Dispose();
base.Dispose(disposing);
}
La clase 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.
Resumen
Ahora tiene un conjunto completo de páginas que realizan sencillas operaciones CRUD para entidades Student
. Ha usado asistentes de MVC para generar elementos de interfaz de usuario para campos de datos. Para más información sobre los asistentes de MVC, consulte Representación de un formulario mediante asistentes de HTML (el artículo es para MVC 3, pero sigue siendo válido para MVC 4).
En el siguiente tutorial podrá expandir la funcionalidad de la página Index mediante la adición de ordenación y paginación.
En el mapa de contenido de acceso a datos de ASP.NET se pueden encontrar vínculos a otros recursos de Entity Framework.