Parte 2. Razor Pages con EF Core en ASP.NET Core: CRUD

De Tom Dykstra, Jeremy Likness y Jon P Smith

En la aplicación web Contoso University se muestra cómo crear aplicaciones web Razor Pages con EF Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.

Si surgen problemas que no puede resolver, descargue la aplicación completada y compare ese código con el que ha creado siguiendo el tutorial.

En este tutorial, se revisa y personaliza el código CRUD (crear, leer, actualizar y eliminar) con scaffolding.

Ningún repositorio

Algunos desarrolladores usan un patrón de repositorio o capa de servicio para crear una capa de abstracción entre la interfaz de usuario (Razor Pages) y la capa de acceso a datos. En este tutorial no se usa. Para minimizar la complejidad y mantener el tutorial centrado en EF Core, el código de EF Core se agrega directamente a las clases de modelo de página.

Actualización de la página de detalles

El código con scaffolding de las páginas Students no incluye datos de inscripción. En esta sección, se agregan inscripciones a la página Details.

Lectura de inscripciones

Para mostrar los datos de inscripción de un alumno en la página, deben leerse estos datos de inscripción. El código con scaffolding de Pages/Students/Details.cshtml.cs solo lee los datos de Student, sin los datos de Enrollment:

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

    Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Reemplace el método OnGetAsync por el código siguiente para leer los datos de inscripción del alumno seleccionado. Los cambios aparecen resaltados.

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

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Los métodos Include y ThenInclude hacen que el contexto cargue la propiedad de navegación Student.Enrollments y, dentro de cada inscripción, la propiedad de navegación Enrollment.Course. Estos métodos se examinan con detalle en el tutorial Lectura de datos relacionados.

El método AsNoTracking mejora el rendimiento en casos en los que las entidades devueltas no se actualizan en el contexto actual. AsNoTracking se describe posteriormente en este tutorial.

Representación de inscripciones

Reemplace el código de Pages/Students/Details.cshtml por el código siguiente para mostrar una lista de las inscripciones. Los cambios aparecen resaltados.

@page
@model ContosoUniversity.Pages.Students.DetailsModel

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

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

El código anterior recorre en bucle las entidades de la propiedad de navegación Enrollments. Para cada inscripción, 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.

Ejecute la aplicación, haga clic en la pestaña Students y después en el vínculo Details de un estudiante. Se muestra la lista de cursos y calificaciones para el alumno seleccionado.

Formas de leer una entidad

En el código generado se usa FirstOrDefaultAsync para leer una entidad. Este método devuelve NULL si no se encuentra nada; de lo contrario, devuelve la primera fila encontrada que satisfaga los criterios de filtro de la consulta. FirstOrDefaultAsync suele ser una opción mejor que las siguientes alternativas:

  • SingleOrDefaultAsync: inicia una excepción si hay más de una entidad que satisface el filtro de consulta. Para determinar si la consulta podría devolver más de una fila, SingleOrDefaultAsync intenta capturar varias filas. Este trabajo adicional no es necesario si la consulta solo puede devolver una entidad, como cuando busca por una clave única.
  • FindAsync: busca una entidad con la clave principal (PK). Si el contexto realiza el seguimiento de una entidad con la clave principal, se devuelve sin una solicitud a la base de datos. Este método está optimizado para buscar una sola entidad, pero no se puede llamar a Include con FindAsync. Por tanto, si se necesitan datos relacionados, FirstOrDefaultAsync es la mejor opción.

Diferencias entre datos de ruta y cadena de consulta

La dirección URL de la página Details es https://localhost:<port>/Students/Details?id=1. El valor de clave principal de la entidad está en la cadena de consulta. Algunos desarrolladores prefieren pasar el valor de clave en los datos de ruta: https://localhost:<port>/Students/Details/1. Para obtener más información, vea Actualización del código generado.

Actualizar la página Create

El código OnPostAsync con scaffolding de la página Create es vulnerable a la publicación excesiva. Reemplace el método OnPostAsync en Pages/Students/Create.cshtml.cs por el código siguiente.

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

El código anterior crea un objeto Student y, después, usa los campos de formulario publicados para actualizar las propiedades del objeto Student. El método TryUpdateModelAsync:

  • Usa los valores de formulario publicados de la propiedad PageContext en el objeto PageModel.
  • Solo actualiza las propiedades enumeradas (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Busca campos de formulario con un prefijo "student". Por ejemplo: Student.FirstMidName. No distingue mayúsculas de minúsculas.
  • Usa el sistema de enlace de modelos para convertir los valores de formulario de cadenas a los tipos Student del modelo. Por ejemplo, EnrollmentDate se convierte en DateTime.

Ejecute la aplicación y cree una entidad Student para probar la página Create.

Publicación excesiva

El uso de TryUpdateModel para actualizar campos con valores enviados es un procedimiento recomendado de seguridad porque evita la publicación excesiva. Por ejemplo, suponga que la entidad Student incluye una propiedad Secret que esta página web no debe actualizar ni agregar:

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

Aunque la aplicación no tenga un campo Secret en la página de Razor de creación o actualización, un hacker podría establecer el valor Secret mediante publicación excesiva. Un hacker podría usar una herramienta como Fiddler, o bien escribir código de JavaScript, para publicar un valor de formulario Secret. El código original no limita los campos que el enlazador de modelos usa cuando crea una instancia Student.

El valor que haya especificado el hacker para el campo de formulario Secret se actualiza en la base de datos. En la imagen siguiente se muestra cómo la herramienta Fiddler agrega el campo Secret, con el valor "OverPost", a los valores de formulario publicados.

Fiddler adding Secret field

El valor "OverPost" se ha agregado correctamente a la propiedad Secret de la fila insertada. Eso sucede aunque el diseñador de la aplicación nunca haya previsto que la propiedad Secret se establezca con la página Create.

Modelo de vista

Los modelos de vista ofrecen una forma alternativa de evitar la publicación excesiva.

El modelo de aplicación se suele denominar modelo de dominio. El modelo de dominio normalmente contiene todas las propiedades requeridas por la entidad correspondiente en la base de datos. El modelo de vista contiene solo las propiedades necesarias para la página de interfaz de usuario como, por ejemplo, la página Create.

Además del modelo de vista, en algunas aplicaciones se usa un modelo de enlace o de entrada para pasar datos entre la clase del modelo de página de Razor Pages y el explorador.

Tenga en cuenta el modelo de vista StudentVM siguiente:

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

En el código siguiente se usa el modelo de vista StudentVM para crear un alumno:

[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

El método SetValues establece los valores de este objeto mediante la lectura de valores de otro objeto PropertyValues. SetValues usa la coincidencia de nombres de propiedad. El tipo de modelo de vista:

  • No es necesario que esté relacionado con el tipo de modelo.
  • Debe tener propiedades que coincidan.

El uso de StudentVM requiere que la página Create use StudentVM en lugar de Student:

@page
@model CreateVMModel

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

<h1>Create</h1>

<h4>Student</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="StudentVM.LastName" class="control-label"></label>
                <input asp-for="StudentVM.LastName" class="form-control" />
                <span asp-validation-for="StudentVM.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.FirstMidName" class="control-label"></label>
                <input asp-for="StudentVM.FirstMidName" class="form-control" />
                <span asp-validation-for="StudentVM.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.EnrollmentDate" class="control-label"></label>
                <input asp-for="StudentVM.EnrollmentDate" class="form-control" />
                <span asp-validation-for="StudentVM.EnrollmentDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Actualizar la página Edit

En Pages/Students/Edit.cshtml.cs, reemplace los métodos OnGetAsync y OnPostAsync por el código siguiente.

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

    Student = await _context.Students.FindAsync(id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

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

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

Los cambios de código son similares a la página Create con algunas excepciones:

  • FirstOrDefaultAsync se ha reemplazado por FindAsync. Cuando no sea necesario incluir datos relacionados, FindAsync es más eficaz.
  • OnPostAsync tiene un parámetro id.
  • El alumno actual se obtiene de la base de datos, en lugar de crear uno vacío.

Ejecute la aplicación y cree y edite un alumno para probarla.

Estados de entidad

El contexto de base de datos realiza el seguimiento de si las entidades en memoria están sincronizadas con sus filas correspondientes en la base de datos. Esta información de sincronización determina qué ocurre cuando se llama a SaveChangesAsync. Por ejemplo, cuando se pasa una nueva entidad al método AddAsync, el estado de esa entidad se establece en Added. Cuando se llama a SaveChangesAsync, el contexto de 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 emite una instrucción INSERT.

  • Unchanged: no es necesario guardar cambios con esta entidad. Una entidad tiene este estado cuando se lee desde la base de datos.

  • Modified: Se han modificado algunos o todos los valores de propiedad de la entidad. El método SaveChanges emite una instrucción UPDATE.

  • Deleted: La entidad se ha marcado para su eliminación. El método SaveChanges emite 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. Se lee una entidad, se realizan cambios y el estado de la entidad se cambia de forma automática a Modified. La llamada a SaveChanges genera una instrucción UPDATE de SQL que solo actualiza las propiedades modificadas.

En una aplicación web, el DbContext que lee una entidad y muestra los datos se elimina después de representar una página. Cuando se llama al método OnPostAsync de una página, se realiza una nueva solicitud web con una instancia nueva de DbContext. Volver a leer la entidad en ese contexto nuevo simula el procesamiento de escritorio.

Actualizar la página Delete

En esta sección, se implementa un mensaje de error personalizado cuando se produce un error en la llamada a SaveChanges.

Reemplace el código de Pages/Students/Delete.cshtml.cs por esto:

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;
        private readonly ILogger<DeleteModel> _logger;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context,
                           ILogger<DeleteModel> logger)
        {
            _context = context;
            _logger = logger;
        }

        [BindProperty]
        public Student Student { get; set; }
        public string ErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
        {
            if (id == null)
            {
                return NotFound();
            }

            Student = await _context.Students
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

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

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = String.Format("Delete {ID} failed. Try again", id);
            }

            return Page();
        }

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

            var student = await _context.Students.FindAsync(id);

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

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException ex)
            {
                _logger.LogError(ex, ErrorMessage);

                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

El código anterior:

  • Agrega Registro.
  • Agrega el parámetro opcional saveChangesError a la firma del método OnGetAsync. saveChangesError indica si se llamó al método después de un error al eliminar el objeto Student.

Es posible que se produzca un error en la operación de eliminación debido a problemas de red transitorios. Los errores de red transitorios son más probables cuando la base de datos está en la nube. El parámetro saveChangesError es false cuando se llama a OnGetAsync de la página Delete desde la interfaz de usuario. Cuando OnPostAsync llama a OnGetAsync, debido a un error en la operación de eliminación, el parámetro saveChangesError es true.

El método OnPostAsync 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. Si se produce un error en Remove:

  • Se detecta la excepción de base de datos.
  • Se llama al método OnGetAsync de las páginas Delete con saveChangesError=true.

Agregue un mensaje de error a Pages/Students/Delete.cshtml:

@page
@model ContosoUniversity.Pages.Students.DeleteModel

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

<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

Ejecute la aplicación y elimine un alumno para probar la página Delete.

Pasos siguientes

En este tutorial, se revisa y personaliza el código CRUD (crear, leer, actualizar y eliminar) con scaffolding.

Ningún repositorio

Algunos desarrolladores usan un patrón de repositorio o capa de servicio para crear una capa de abstracción entre la interfaz de usuario (Razor Pages) y la capa de acceso a datos. En este tutorial no se usa. Para minimizar la complejidad y mantener el tutorial centrado en EF Core, el código de EF Core se agrega directamente a las clases de modelo de página.

Actualización de la página de detalles

El código con scaffolding de las páginas Students no incluye datos de inscripción. En esta sección, se agregan inscripciones a la página Details.

Lectura de inscripciones

Para mostrar los datos de inscripción de un alumno en la página, deben leerse estos datos de inscripción. El código con scaffolding de Pages/Students/Details.cshtml.cs solo lee los datos de Student, sin los datos de Enrollment:

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

    Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Reemplace el método OnGetAsync por el código siguiente para leer los datos de inscripción del alumno seleccionado. Los cambios aparecen resaltados.

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

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Los métodos Include y ThenInclude hacen que el contexto cargue la propiedad de navegación Student.Enrollments y, dentro de cada inscripción, la propiedad de navegación Enrollment.Course. Estos métodos se examinan con detalle en el tutorial Lectura de datos relacionados.

El método AsNoTracking mejora el rendimiento en casos en los que las entidades devueltas no se actualizan en el contexto actual. AsNoTracking se describe posteriormente en este tutorial.

Representación de inscripciones

Reemplace el código de Pages/Students/Details.cshtml por el código siguiente para mostrar una lista de las inscripciones. Los cambios aparecen resaltados.

@page
@model ContosoUniversity.Pages.Students.DetailsModel

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

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

El código anterior recorre en bucle las entidades de la propiedad de navegación Enrollments. Para cada inscripción, 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.

Ejecute la aplicación, haga clic en la pestaña Students y después en el vínculo Details de un estudiante. Se muestra la lista de cursos y calificaciones para el alumno seleccionado.

Formas de leer una entidad

En el código generado se usa FirstOrDefaultAsync para leer una entidad. Este método devuelve NULL si no se encuentra nada; de lo contrario, devuelve la primera fila encontrada que satisfaga los criterios de filtro de la consulta. FirstOrDefaultAsync suele ser una opción mejor que las siguientes alternativas:

  • SingleOrDefaultAsync: inicia una excepción si hay más de una entidad que satisface el filtro de consulta. Para determinar si la consulta podría devolver más de una fila, SingleOrDefaultAsync intenta capturar varias filas. Este trabajo adicional no es necesario si la consulta solo puede devolver una entidad, como cuando busca por una clave única.
  • FindAsync: busca una entidad con la clave principal (PK). Si el contexto realiza el seguimiento de una entidad con la clave principal, se devuelve sin una solicitud a la base de datos. Este método está optimizado para buscar una sola entidad, pero no se puede llamar a Include con FindAsync. Por tanto, si se necesitan datos relacionados, FirstOrDefaultAsync es la mejor opción.

Diferencias entre datos de ruta y cadena de consulta

La dirección URL de la página Details es https://localhost:<port>/Students/Details?id=1. El valor de clave principal de la entidad está en la cadena de consulta. Algunos desarrolladores prefieren pasar el valor de clave en los datos de ruta: https://localhost:<port>/Students/Details/1. Para obtener más información, vea Actualización del código generado.

Actualizar la página Create

El código OnPostAsync con scaffolding de la página Create es vulnerable a la publicación excesiva. Reemplace el método OnPostAsync en Pages/Students/Create.cshtml.cs por el código siguiente.

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

El código anterior crea un objeto Student y, después, usa los campos de formulario publicados para actualizar las propiedades del objeto Student. El método TryUpdateModelAsync:

  • Usa los valores de formulario publicados de la propiedad PageContext en el objeto PageModel.
  • Solo actualiza las propiedades enumeradas (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Busca campos de formulario con un prefijo "student". Por ejemplo: Student.FirstMidName. No distingue mayúsculas de minúsculas.
  • Usa el sistema de enlace de modelos para convertir los valores de formulario de cadenas a los tipos Student del modelo. Por ejemplo, EnrollmentDate se convierte en DateTime.

Ejecute la aplicación y cree una entidad Student para probar la página Create.

Publicación excesiva

El uso de TryUpdateModel para actualizar campos con valores enviados es un procedimiento recomendado de seguridad porque evita la publicación excesiva. Por ejemplo, suponga que la entidad Student incluye una propiedad Secret que esta página web no debe actualizar ni agregar:

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

Aunque la aplicación no tenga un campo Secret en la página de Razor de creación o actualización, un hacker podría establecer el valor Secret mediante publicación excesiva. Un hacker podría usar una herramienta como Fiddler, o bien escribir código de JavaScript, para publicar un valor de formulario Secret. El código original no limita los campos que el enlazador de modelos usa cuando crea una instancia Student.

El valor que haya especificado el hacker para el campo de formulario Secret se actualiza en la base de datos. En la imagen siguiente se muestra cómo la herramienta Fiddler agrega el campo Secret, con el valor "OverPost", a los valores de formulario publicados.

Fiddler adding Secret field

El valor "OverPost" se ha agregado correctamente a la propiedad Secret de la fila insertada. Eso sucede aunque el diseñador de la aplicación nunca haya previsto que la propiedad Secret se establezca con la página Create.

Modelo de vista

Los modelos de vista ofrecen una forma alternativa de evitar la publicación excesiva.

El modelo de aplicación se suele denominar modelo de dominio. El modelo de dominio normalmente contiene todas las propiedades requeridas por la entidad correspondiente en la base de datos. El modelo de vista contiene solo las propiedades necesarias para la página de interfaz de usuario como, por ejemplo, la página Create.

Además del modelo de vista, en algunas aplicaciones se usa un modelo de enlace o de entrada para pasar datos entre la clase del modelo de página de Razor Pages y el explorador.

Tenga en cuenta el modelo de vista StudentVM siguiente:

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

En el código siguiente se usa el modelo de vista StudentVM para crear un alumno:

[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

El método SetValues establece los valores de este objeto mediante la lectura de valores de otro objeto PropertyValues. SetValues usa la coincidencia de nombres de propiedad. El tipo de modelo de vista:

  • No es necesario que esté relacionado con el tipo de modelo.
  • Debe tener propiedades que coincidan.

El uso de StudentVM requiere que la página Create use StudentVM en lugar de Student:

@page
@model CreateVMModel

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

<h1>Create</h1>

<h4>Student</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="StudentVM.LastName" class="control-label"></label>
                <input asp-for="StudentVM.LastName" class="form-control" />
                <span asp-validation-for="StudentVM.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.FirstMidName" class="control-label"></label>
                <input asp-for="StudentVM.FirstMidName" class="form-control" />
                <span asp-validation-for="StudentVM.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.EnrollmentDate" class="control-label"></label>
                <input asp-for="StudentVM.EnrollmentDate" class="form-control" />
                <span asp-validation-for="StudentVM.EnrollmentDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Actualizar la página Edit

En Pages/Students/Edit.cshtml.cs, reemplace los métodos OnGetAsync y OnPostAsync por el código siguiente.

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

    Student = await _context.Students.FindAsync(id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

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

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

Los cambios de código son similares a la página Create con algunas excepciones:

  • FirstOrDefaultAsync se ha reemplazado por FindAsync. Cuando no sea necesario incluir datos relacionados, FindAsync es más eficaz.
  • OnPostAsync tiene un parámetro id.
  • El alumno actual se obtiene de la base de datos, en lugar de crear uno vacío.

Ejecute la aplicación y cree y edite un alumno para probarla.

Estados de entidad

El contexto de base de datos realiza el seguimiento de si las entidades en memoria están sincronizadas con sus filas correspondientes en la base de datos. Esta información de sincronización determina qué ocurre cuando se llama a SaveChangesAsync. Por ejemplo, cuando se pasa una nueva entidad al método AddAsync, el estado de esa entidad se establece en Added. Cuando se llama a SaveChangesAsync, el contexto de 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 emite una instrucción INSERT.

  • Unchanged: no es necesario guardar cambios con esta entidad. Una entidad tiene este estado cuando se lee desde la base de datos.

  • Modified: Se han modificado algunos o todos los valores de propiedad de la entidad. El método SaveChanges emite una instrucción UPDATE.

  • Deleted: La entidad se ha marcado para su eliminación. El método SaveChanges emite 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. Se lee una entidad, se realizan cambios y el estado de la entidad se cambia de forma automática a Modified. La llamada a SaveChanges genera una instrucción UPDATE de SQL que solo actualiza las propiedades modificadas.

En una aplicación web, el DbContext que lee una entidad y muestra los datos se elimina después de representar una página. Cuando se llama al método OnPostAsync de una página, se realiza una nueva solicitud web con una instancia nueva de DbContext. Volver a leer la entidad en ese contexto nuevo simula el procesamiento de escritorio.

Actualizar la página Delete

En esta sección, se implementa un mensaje de error personalizado cuando se produce un error en la llamada a SaveChanges.

Reemplace el código de Pages/Students/Delete.cshtml.cs por esto:

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;
        private readonly ILogger<DeleteModel> _logger;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context,
                           ILogger<DeleteModel> logger)
        {
            _context = context;
            _logger = logger;
        }

        [BindProperty]
        public Student Student { get; set; }
        public string ErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
        {
            if (id == null)
            {
                return NotFound();
            }

            Student = await _context.Students
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

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

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = String.Format("Delete {ID} failed. Try again", id);
            }

            return Page();
        }

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

            var student = await _context.Students.FindAsync(id);

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

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException ex)
            {
                _logger.LogError(ex, ErrorMessage);

                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

El código anterior:

  • Agrega Registro.
  • Agrega el parámetro opcional saveChangesError a la firma del método OnGetAsync. saveChangesError indica si se llamó al método después de un error al eliminar el objeto Student.

Es posible que se produzca un error en la operación de eliminación debido a problemas de red transitorios. Los errores de red transitorios son más probables cuando la base de datos está en la nube. El parámetro saveChangesError es false cuando se llama a OnGetAsync de la página Delete desde la interfaz de usuario. Cuando OnPostAsync llama a OnGetAsync, debido a un error en la operación de eliminación, el parámetro saveChangesError es true.

El método OnPostAsync 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. Si se produce un error en Remove:

  • Se detecta la excepción de base de datos.
  • Se llama al método OnGetAsync de las páginas Delete con saveChangesError=true.

Agregue un mensaje de error a Pages/Students/Delete.cshtml:

@page
@model ContosoUniversity.Pages.Students.DeleteModel

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

<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

Ejecute la aplicación y elimine un alumno para probar la página Delete.

Pasos siguientes

En este tutorial, se revisa y personaliza el código CRUD (crear, leer, actualizar y eliminar) con scaffolding.

Ningún repositorio

Algunos desarrolladores usan un patrón de repositorio o capa de servicio para crear una capa de abstracción entre la interfaz de usuario (Razor Pages) y la capa de acceso a datos. En este tutorial no se usa. Para minimizar la complejidad y mantener el tutorial centrado en EF Core, el código de EF Core se agrega directamente a las clases de modelo de página.

Actualización de la página de detalles

El código con scaffolding de las páginas Students no incluye datos de inscripción. En esta sección, se agregan inscripciones a la página Details.

Lectura de inscripciones

Para mostrar los datos de inscripción de un alumno en la página, es necesario leerlos. El código con scaffolding de Pages/Students/Details.cshtml.cs solo lee los datos de Student, sin los datos de Enrollment:

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

    Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Reemplace el método OnGetAsync por el código siguiente para leer los datos de inscripción del alumno seleccionado. Los cambios aparecen resaltados.

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

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Los métodos Include y ThenInclude hacen que el contexto cargue la propiedad de navegación Student.Enrollments y, dentro de cada inscripción, la propiedad de navegación Enrollment.Course. Estos métodos se examinan con detalle en el tutorial Lectura de datos relacionados.

El método AsNoTracking mejora el rendimiento en casos en los que las entidades devueltas no se actualizan en el contexto actual. AsNoTracking se describe posteriormente en este tutorial.

Representación de inscripciones

Reemplace el código de Pages/Students/Details.cshtml por el código siguiente para mostrar una lista de las inscripciones. Los cambios aparecen resaltados.

@page
@model ContosoUniversity.Pages.Students.DetailsModel

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

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

El código anterior recorre en bucle las entidades de la propiedad de navegación Enrollments. Para cada inscripción, 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.

Ejecute la aplicación, haga clic en la pestaña Students y después en el vínculo Details de un estudiante. Se muestra la lista de cursos y calificaciones para el alumno seleccionado.

Formas de leer una entidad

En el código generado se usa FirstOrDefaultAsync para leer una entidad. Este método devuelve NULL si no se encuentra nada; de lo contrario, devuelve la primera fila encontrada que satisfaga los criterios de filtro de la consulta. FirstOrDefaultAsync suele ser una opción mejor que las siguientes alternativas:

  • SingleOrDefaultAsync: inicia una excepción si hay más de una entidad que satisface el filtro de consulta. Para determinar si la consulta podría devolver más de una fila, SingleOrDefaultAsync intenta capturar varias filas. Este trabajo adicional no es necesario si la consulta solo puede devolver una entidad, como cuando busca por una clave única.
  • FindAsync: busca una entidad con la clave principal (PK). Si el contexto realiza el seguimiento de una entidad con la clave principal, se devuelve sin una solicitud a la base de datos. Este método está optimizado para buscar una sola entidad, pero no se puede llamar a Include con FindAsync. Por tanto, si se necesitan datos relacionados, FirstOrDefaultAsync es la mejor opción.

Diferencias entre datos de ruta y cadena de consulta

La dirección URL de la página Details es https://localhost:<port>/Students/Details?id=1. El valor de clave principal de la entidad está en la cadena de consulta. Algunos desarrolladores prefieren pasar el valor de clave en los datos de ruta: https://localhost:<port>/Students/Details/1. Para obtener más información, vea Actualización del código generado.

Actualizar la página Create

El código OnPostAsync con scaffolding de la página Create es vulnerable a la publicación excesiva. Reemplace el método OnPostAsync en Pages/Students/Create.cshtml.cs por el código siguiente.

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

El código anterior crea un objeto Student y, después, usa los campos de formulario publicados para actualizar las propiedades del objeto Student. El método TryUpdateModelAsync:

  • Usa los valores de formulario publicados de la propiedad PageContext en el objeto PageModel.
  • Solo actualiza las propiedades enumeradas (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Busca campos de formulario con un prefijo "student". Por ejemplo: Student.FirstMidName. No distingue mayúsculas de minúsculas.
  • Usa el sistema de enlace de modelos para convertir los valores de formulario de cadenas a los tipos Student del modelo. Por ejemplo, EnrollmentDate se debe convertir a DateTime.

Ejecute la aplicación y cree una entidad Student para probar la página Create.

Publicación excesiva

El uso de TryUpdateModel para actualizar campos con valores enviados es un procedimiento recomendado de seguridad porque evita la publicación excesiva. Por ejemplo, suponga que la entidad Student incluye una propiedad Secret que esta página web no debe actualizar ni agregar:

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

Aunque la aplicación no tenga un campo Secret en la página de Razor de creación o actualización, un hacker podría establecer el valor Secret mediante publicación excesiva. Un hacker podría usar una herramienta como Fiddler, o bien escribir código de JavaScript, para publicar un valor de formulario Secret. El código original no limita los campos que el enlazador de modelos usa cuando crea una instancia Student.

El valor que haya especificado el hacker para el campo de formulario Secret se actualiza en la base de datos. En la imagen siguiente se muestra cómo la herramienta Fiddler agrega el campo Secret (con el valor "OverPost") a los valores de formulario enviados.

Fiddler adding Secret field

El valor "OverPost" se ha agregado correctamente a la propiedad Secret de la fila insertada. Eso sucede aunque el diseñador de la aplicación nunca haya previsto que la propiedad Secret se establezca con la página Create.

Modelo de vista

Los modelos de vista ofrecen una forma alternativa de evitar la publicación excesiva.

El modelo de aplicación se suele denominar modelo de dominio. El modelo de dominio normalmente contiene todas las propiedades requeridas por la entidad correspondiente en la base de datos. El modelo de vista contiene solo las propiedades necesarias para la interfaz de usuario para la que se usa (por ejemplo, la página Create).

Además del modelo de vista, en algunas aplicaciones se usa un modelo de enlace o de entrada para pasar datos entre la clase del modelo de página de Razor Pages y el explorador.

Tenga en cuenta el modelo de vista Student siguiente:

using System;

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

En el código siguiente se usa el modelo de vista StudentVM para crear un alumno:

[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

El método SetValues establece los valores de este objeto mediante la lectura de valores de otro objeto PropertyValues. SetValues usa la coincidencia de nombres de propiedad. No es necesario que el tipo de modelo de vista esté relacionado con el tipo de modelo, basta con que tenga propiedades que coincidan.

El uso de StudentVM requiere que se actualice Create.cshtml para usar StudentVM en lugar de Student.

Actualizar la página Edit

En Pages/Students/Edit.cshtml.cs, reemplace los métodos OnGetAsync y OnPostAsync por el código siguiente.

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

    Student = await _context.Students.FindAsync(id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

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

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

Los cambios de código son similares a la página Create con algunas excepciones:

  • FirstOrDefaultAsync se ha reemplazado por FindAsync. Cuando no se necesitan datos relacionados incluidos, FindAsync es más eficaz.
  • OnPostAsync tiene un parámetro id.
  • El alumno actual se obtiene de la base de datos, en lugar de crear uno vacío.

Ejecute la aplicación y cree y edite un alumno para probarla.

Estados de entidad

El contexto de base de datos realiza el seguimiento de si las entidades en memoria están sincronizadas con sus filas correspondientes en la base de datos. Esta información de sincronización determina qué ocurre cuando se llama a SaveChangesAsync. Por ejemplo, cuando se pasa una nueva entidad al método AddAsync, el estado de esa entidad se establece en Added. Cuando se llama a SaveChangesAsync, el contexto de 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 emite una instrucción INSERT.

  • Unchanged: no es necesario guardar cambios con esta entidad. Una entidad tiene este estado cuando se lee desde la base de datos.

  • Modified: Se han modificado algunos o todos los valores de propiedad de la entidad. El método SaveChanges emite una instrucción UPDATE.

  • Deleted: La entidad se ha marcado para su eliminación. El método SaveChanges emite 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. Se lee una entidad, se realizan cambios y el estado de la entidad se cambia de forma automática a Modified. La llamada a SaveChanges genera una instrucción UPDATE de SQL que solo actualiza las propiedades modificadas.

En una aplicación web, el DbContext que lee una entidad y muestra los datos se elimina después de representar una página. Cuando se llama al método OnPostAsync de una página, se realiza una nueva solicitud web con una instancia nueva de DbContext. Volver a leer la entidad en ese contexto nuevo simula el procesamiento de escritorio.

Actualizar la página Delete

En esta sección, se implementa un mensaje de error personalizado cuando se produce un error en la llamada a SaveChanges.

Reemplace el código de Pages/Students/Delete.cshtml.cs por el código siguiente. Los cambios se resaltan (a excepción de la limpieza de las instrucciones using).

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Student Student { get; set; }
        public string ErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
        {
            if (id == null)
            {
                return NotFound();
            }

            Student = await _context.Students
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

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

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = "Delete failed. Try again";
            }

            return Page();
        }

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

            var student = await _context.Students.FindAsync(id);

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

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException /* ex */)
            {
                //Log the error (uncomment ex variable name and write a log.)
                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

En el código anterior se agrega el parámetro opcional saveChangesError a la firma del método OnGetAsync. saveChangesError indica si se llamó al método después de un error al eliminar el objeto Student. Es posible que se produzca un error en la operación de eliminación debido a problemas de red transitorios. Los errores de red transitorios son más probables cuando la base de datos está en la nube. El parámetro saveChangesError es false cuando se llama a OnGetAsync de la página Delete desde la interfaz de usuario. Cuando OnPostAsync llama a OnGetAsync (debido a un error en la operación de eliminación), el parámetro saveChangesError es true.

El método OnPostAsync 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. Si se produce un error en Remove:

  • Se detecta la excepción de base de datos.
  • Se llama al método OnGetAsync de la página Delete con saveChangesError=true.

Agregue un mensaje de error a la página de Razor Delete (Pages/Students/Delete.cshtml):

@page
@model ContosoUniversity.Pages.Students.DeleteModel

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

<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

Ejecute la aplicación y elimine un alumno para probar la página Delete.

Pasos siguientes