Parte 2. Razor Pages con EF Core en ASP.NET Core: CRUD
Nota:
Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión de .NET 9 de este artículo.
Advertencia
Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta la versión .NET 8 de este artículo.
Importante
Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.
Para la versión actual, consulte la versión de .NET 9 de este artículo.
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
conFindAsync
. 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 enDateTime
.
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.
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ámetroid
.- 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étodoSaveChanges
emite una instrucciónINSERT
.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étodoSaveChanges
emite una instrucciónUPDATE
.Deleted
: La entidad se ha marcado para su eliminación. El métodoSaveChanges
emite una instrucciónDELETE
.Detached
: El contexto de base de datos no está realizando el seguimiento de la entidad.
En una aplicación de escritorio, los cambios de estado normalmente se establecen de forma automática. 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étodoOnGetAsync
.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 consaveChangesError=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
conFindAsync
. 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 enDateTime
.
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.
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ámetroid
.- 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étodoSaveChanges
emite una instrucciónINSERT
.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étodoSaveChanges
emite una instrucciónUPDATE
.Deleted
: La entidad se ha marcado para su eliminación. El métodoSaveChanges
emite una instrucciónDELETE
.Detached
: El contexto de base de datos no está realizando el seguimiento de la entidad.
En una aplicación de escritorio, los cambios de estado normalmente se establecen de forma automática. 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étodoOnGetAsync
.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 consaveChangesError=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
conFindAsync
. 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.
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ámetroid
.- 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étodoSaveChanges
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étodoSaveChanges
emite una instrucción UPDATE.Deleted
: La entidad se ha marcado para su eliminación. El métodoSaveChanges
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 consaveChangesError=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.