Compartir a través de


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 encuentra con un problema que no puede resolver, descargue el capítulo completado e intente reproducir el problema. Por lo general, puede encontrar la solución al problema comparando su 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:

Screenshot showing the Contoso University Student Details page.

Screenshot showing the Contoso University Student Edit page.

Screenshot showing the Contoso University Student Create page.

Screenshot that shows the Student Delete page.

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.

  1. 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>
    
  2. Después del campo EnrollmentDate e inmediatamente antes de la etiqueta fieldset 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 entidad Enrollment de la propiedad, se muestra el título del curso y la calificación. El título del curso se recupera de la entidad Course almacenada en la propiedad de navegación Course de la entidad Enrollments. 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ón Courses, 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).

  3. 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:

    Student_Details_page

Actualización de la página Create

  1. En Controllers\StudentController.cs, reemplace el método de acción HttpPost``Create por el código siguiente para agregar un bloque try-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 entidades Students 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 entidad Student mediante valores de propiedad de la colección Form).

    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*.
  1. Para ejecutar la página, seleccione la pestaña Students y, luego, haga clic en Crear nuevo.

    Student_Create_page

    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.

    Students_Create_page_error_message

    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.

    Students_Index_page_with_new_student

Actualización de la página Edit POST

En Controllers\StudentController.cs, el método HttpGetEdit (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 HttpPostEdit 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 HttpPostCreate. 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étodo SaveChanges debe emitir una instrucción INSERT.
  • Unchanged. No es necesario hacer nada con esta entidad mediante el método SaveChanges. 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étodo SaveChanges debe emitir una instrucción UPDATE.
  • Deleted. La entidad se ha marcado para su eliminación. El método SaveChanges debe emitir una instrucción DELETE.
  • 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 HttpPostEdit, se realiza una nueva solicitud y se tiene una nueva instancia de DbContext, por lo que tiene que establecer manualmente el estado de la entidad en Modified.. A continuación, 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 desea 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 HttpPostEdit. 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.

Student_Edit_page

Cambie algunos de los datos y haga clic en Guardar. Verá los datos modificados en la página Index.

Students_Index_page_after_edit

Actualización de la página Delete

En Controllers\StudentController.cs, el código de plantilla del método HttpGetDelete usa el método Find para recuperar la entidad Student seleccionada, como se vio 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 HttpPostDelete y, después, ese método es el que realiza la operación de eliminación.

Agregará un bloque try-catch al método HttpPostDelete para controlar los errores que puedan producirse cuando se actualice la base de datos. Si se produce un error, el método HttpPostDelete llama al método HttpGetDelete, 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.

  1. Reemplace el método de acción HttpGetDelete 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étodo HttpGetDelete sin un error anterior. Cuando se llama por medio del método HttpPostDelete 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.

  2. Reemplace el método de acción HttpPostDelete (denominado DeleteConfirmed) 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 a SaveChanges, se genera un comando DELETE de SQL. También ha cambiado el nombre del método de acción de DeleteConfirmed a Delete. El código con andamiaje asignó al método HttpPostDelete el nombre DeleteConfirmed para proporcionar al método HttpPost 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ón HttpPost y HttpGet.

    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 y Remove 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 en Deleted. Eso es todo lo que necesita Entity Framework para eliminar la entidad.

    Como se ha indicado, el método HttpGetDelete 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.

  3. En Views/Student/Delete.cshtml, agregue un mensaje de error entre los encabezados h2 y h3, 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:

    Student_Delete_page

  4. 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.