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. Sustituya el método OnPostAsync de Pages/Students/Create.cshtml.cs con 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.

Campo Secret agregado por Fiddler

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 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, sustituya los métodos OnGetAsync y OnPostAsync con 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 con 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 entidad nueva 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.

Sustituya el código de Pages/Students/Delete.cshtml.cs con el código siguiente:

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 leer 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. Sustituya el método OnPostAsync de Pages/Students/Create.cshtml.cs con 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.

Campo Secret agregado por Fiddler

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 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, sustituya los métodos OnGetAsync y OnPostAsync con 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 con 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 entidad nueva 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.

Sustituya el código de Pages/Students/Delete.cshtml.cs con 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

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

Para minimizar la complejidad y mantener estos tutoriales centrados en EF Core, en los modelos de página se usa código de EF Core. 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 se examinan las páginas de Razor Pages Create, Edit, Delete y Details de la carpeta Students.

En el código con scaffolding se usa el modelo siguiente para las páginas Create, Edit y Delete:

  • Obtenga y muestre los datos solicitados con el método HTTP GET OnGetAsync.
  • Guarde los cambios en los datos con el método HTTP POST OnPostAsync.

Las páginas Index y Details obtienen y muestran los datos solicitados con el método HTTP GET OnGetAsync

SingleOrDefaultAsync frente a FirstOrDefaultAsync

En el código generado se usa FirstOrDefaultAsync, que normalmente es preferible a SingleOrDefaultAsync.

FirstOrDefaultAsync es más eficaz que SingleOrDefaultAsync para capturar una entidad:

  • A menos que el código necesite comprobar que no hay más de una entidad devuelta por la consulta.
  • SingleOrDefaultAsync captura más datos y realiza trabajo innecesario.
  • SingleOrDefaultAsync inicia una excepción si hay más de una entidad que se ajuste a la parte del filtro.
  • FirstOrDefaultAsync no inicia una excepción si hay más de una entidad que se ajuste a la parte del filtro.

FindAsync

En gran parte del código con scaffolding, se puede usar FindAsync en lugar de FirstOrDefaultAsync.

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.
  • Es sencillo y conciso.
  • Está optimizado para buscar una sola entidad.
  • Puede tener ventajas de rendimiento en algunas situaciones, pero rara vez se produce en aplicaciones web normales.
  • Usa implícitamente FirstAsync en lugar de SingleAsync.

Sin embargo, si quiere aplicar Include a otras entidades, FindAsync ya no resulta apropiado. Esto significa que puede que necesite descartar FindAsync y cambiar a una consulta cuando la aplicación progrese.

Personalizar la página de detalles

Vaya a la página Pages/Students. Los vínculos Edit, Details y Delete son generados por la Asistente de etiquetas delimitadoras del archivo Pages/Students/Index.cshtml.

<td>
    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>

Ejecute la aplicación y haga clic en un vínculo Details. La dirección URL tiene el formato http://localhost:5000/Students/Details?id=2. Se pasa Student ID mediante una cadena de consulta (?id=2).

Actualice las páginas de Razor Edit, Details y Delete para usar la plantilla de ruta "{id:int}". Cambie la directiva de página de cada una de estas páginas de @page a @page "{id:int}".

Una solicitud a la página con la plantilla de ruta "{id:int}" que no incluya un valor de ruta entero devolverá un error HTTP 404 (no encontrado). Por ejemplo, http://localhost:5000/Students/Details devuelve un error 404. Para que el identificador sea opcional, anexe ? a la restricción de ruta:

@page "{id:int?}"

Ejecute la aplicación, haga clic en un vínculo Details y compruebe que la dirección URL pasa el identificador como datos de ruta ( http://localhost:5000/Students/Details/2 ).

No cambie globalmente @page por @page "{id:int}"; esta acción rompería los vínculos a las páginas Home y Create.

El código con scaffolding de la página Students Index no incluye la propiedad Enrollments. En esta sección, se mostrará el contenido de la colección Enrollments en la página Details.

El método OnGetAsync de Pages/Students/Details.cshtml.cs usa el método FirstOrDefaultAsync para recuperar una única entidad Student. Agregue el código resaltado siguiente:

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

    Student = await _context.Student
                        .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 de 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.

Abra Pages/Students/Details.cshtml. Agregue el siguiente código resaltado para mostrar una lista de las inscripciones:

@page "{id:int}"
@model ContosoUniversity.Pages.Students.DetailsModel

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

<h2>Details</h2>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd>
            <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>

Si la sangría de código no es correcta después de pegar el código, presione CTRL-K-D para corregirlo.

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.

Actualizar la página Create

Actualice el método OnPostAsync de Pages/Students/Create.cshtml.cs con el código siguiente:

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

    var emptyStudent = new Student();

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

    return null;
}

TryUpdateModelAsync

Examine el código de TryUpdateModelAsync:


var emptyStudent = new Student();

if (await TryUpdateModelAsync<Student>(
    emptyStudent,
    "student",   // Prefix for form value.
    s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{

En el código anterior, TryUpdateModelAsync<Student> intenta actualizar el objeto emptyStudent mediante los valores de formulario enviados desde la propiedad PageContext del PageModel. TryUpdateModelAsync solo actualiza las propiedades enumeradas (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).

En el ejemplo anterior:

  • El segundo argumento ("student", // Prefix) es el prefijo que se usa para buscar valores. No distingue mayúsculas de minúsculas.
  • Los valores de formulario enviados se convierten a los tipos del modelo Student mediante el enlace de modelos.

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.

Campo Secret agregado por Fiddler

El valor "OverPost" se ha agregado correctamente a la propiedad Secret de la fila insertada. El diseñador de aplicaciones no había previsto que la propiedad Secret se estableciera con la página Create.

Modelo de vista

Normalmente, un modelo de vista contiene un subconjunto de las propiedades incluidas en el modelo que usa la aplicación. 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 capa de interfaz de usuario (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; }
    }
}

Los modelos de vista ofrecen una forma alternativa de evitar la publicación excesiva. El modelo de vista contiene solo las propiedades que se van a ver (mostrar) o actualizar.

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 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 CreateVM.cshtml para usar StudentVM en lugar de Student.

En Razor Pages, la clase derivada PageModel es el modelo de vista.

Actualizar la página Edit

Actualice el modelo de página para la página Edit. Los cambios más importantes aparecen resaltados:

public class EditModel : PageModel
{
    private readonly SchoolContext _context;

    public EditModel(SchoolContext context)
    {
        _context = context;
    }

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

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

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

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

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

        var studentToUpdate = await _context.Student.FindAsync(id);

        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:

  • OnPostAsync tiene un parámetro id opcional.
  • El estudiante actual se obtiene de la base de datos, en lugar de crear un estudiante vacío.
  • FirstOrDefaultAsync se ha reemplazado con FindAsync. FindAsync es una buena elección cuando se selecciona una entidad de la clave principal. Vea FindAsync para obtener más información.

Probar las páginas Edit y Create

Cree y modifique algunas entidades Student.

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. La información de sincronización del contexto de base de datos determina qué ocurre cuando se llama a SaveChangesAsync. Por ejemplo, cuando se pasa una entidad nueva 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 automáticamente 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 agrega código para implementar un mensaje de error personalizado cuando se produce un error en la llamada a SaveChanges. Agregue una cadena para contener los posibles mensajes de error:

public class DeleteModel : PageModel
{
    private readonly SchoolContext _context;

    public DeleteModel(SchoolContext context)
    {
        _context = context;
    }

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

Reemplace el método OnGetAsync con el código siguiente:

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

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

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

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

    return Page();
}

El código anterior contiene el parámetro opcional saveChangesError. 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 en la nube. 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 de las páginas Delete

Reemplace OnPostAsync por el código siguiente:

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

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

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

    try
    {
        _context.Student.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 recupera la entidad seleccionada y después se 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.

Actualización de la página de Razor Delete

Agregue el siguiente mensaje de error resaltado a la página de Razor Delete.

@page "{id:int}"
@model ContosoUniversity.Pages.Students.DeleteModel

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

<h2>Delete</h2>

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

<h3>Are you sure you want to delete this?</h3>
<div>

Pruebe Delete.

Errores comunes

Students/Index u otros vínculos no funcionan:

Compruebe que la página de Razor contiene la directiva @page correcta. Por ejemplo, la página de Razor Students/Index no contiene ninguna plantilla de ruta:

@page "{id:int}"

Cada página de Razor debe incluir la directiva @page.

Recursos adicionales