Часть 2. Razor Страницы с EF Core ASP.NET Core — CRUD
Примечание.
Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 9 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 9 этой статьи.
Внимание
Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
В текущем выпуске см . версию .NET 9 этой статьи.
Авторы: Том Дайкстра (Tom Dykstra), Джереми Ликнесс (Jeremy Likness) и Йон П. Смит (Jon P Smith)
Веб-приложение Contoso University демонстрирует создание Razor веб-приложений Pages с помощью EF Core Visual Studio. Сведения о серии руководств см. в первом руководстве серии.
При возникновении проблем, которые вам не удается устранить, скачайте готовое приложение и сравните его код с тем, который вы создали в процессе работы с этим руководством.
В этом учебнике описывается проверка и настройка шаблонного кода операций CRUD (создание, чтение, обновление и удаление).
Репозиторий отсутствует.
Некоторые разработчики используют уровень служб или шаблон репозитория, чтобы создать уровень абстракции между пользовательским интерфейсом (Razor Pages) и уровнем доступа к данным. В данном учебнике этого не делается. Чтобы свести к минимуму сложность и сосредоточиться на руководстве EF Core, EF Core код добавляется непосредственно в классы модели страницы.
Обновление страницы Details (сведения)
Шаблонный код для страниц учащихся не включает в себя сведения о регистрации. В этом разделе показано, как регистрации добавляются на страницу Details
.
Считывание сведений о регистрации
Чтобы отобразить сведения о регистрации учащегося на странице, эти сведения необходимо считать. Шаблонный код в файле Pages/Students/Details.cshtml.cs
считывает только сведения Student
, но не сведения 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();
}
Замените метод OnGetAsync
приведенным ниже кодом для считывания сведений о регистрации для выбранного учащегося. Изменения выделены.
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();
}
Методы Include и ThenInclude инструктируют контекст для загрузки свойства навигации Student.Enrollments
, а также свойства навигации Enrollment.Course
в пределах каждой регистрации. Эти методы более подробно рассматриваются в руководстве, посвященном чтению связанных данных.
Метод AsNoTracking повышает производительность в сценариях, когда возвращаемые сущности не обновляются в текущем контексте. AsNoTracking
рассматривается позднее в этом учебнике.
Отображение регистраций
Замените код в Pages/Students/Details.cshtml
следующем коде, чтобы отобразить список регистраций. Изменения выделены.
@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>
Приведенный выше код циклически обрабатывает сущности в свойстве навигации Enrollments
. Для каждой регистрации он отображает название курса и оценку. Название курса извлекается из сущности Course
, которая хранится в свойстве навигации Course
сущности Enrollments.
Запустите приложение, выберите вкладку Students (Учащиеся) и щелкните ссылку Details (Сведения) для учащегося. Отобразится список курсов и оценок для выбранного учащегося.
Способы чтения одной сущности
В созданном коде для чтения одной сущности используется метод FirstOrDefaultAsync. Этот метод возвращает значение NULL, если ничего не найдено. В противном случае возвращается первая найденная строка, удовлетворяющая условиям фильтра запросов. FirstOrDefaultAsync
обычно предпочтительнее, чем следующие варианты:
- SingleOrDefaultAsync — вызывает исключение при наличии нескольких сущностей, соответствующих фильтру запросов. Для определения того, может ли запрос вернуть более одной строки,
SingleOrDefaultAsync
пытается получить несколько строк. Это дополнительное действие не требуется, если запрос может вернуть только одну сущность, как при поиске по уникальному ключу. - FindAsync — находит сущность с первичным ключом. Если сущность с первичным ключом отслеживается контекстом, она возвращается без запроса к базе данных. Этот метод оптимизирован для поиска одной сущности, однако вызвать
Include
сFindAsync
невозможно. Поэтому если требуются связанные данные, лучше использоватьFirstOrDefaultAsync
.
Данные маршрута или строка запроса
URL-адрес страницы сведений — https://localhost:<port>/Students/Details?id=1
. Значение первичного ключа сущности содержится в строке запроса. Некоторые разработчики предпочитают передавать значение ключа в данных маршрута: https://localhost:<port>/Students/Details/1
. Дополнительные сведения см. в разделе Обновление созданного кода.
Обновление страницы Create
Шаблонный код OnPostAsync
для страницы создания уязвим к чрезмерной передаче данных. Замените метод OnPostAsync
в файле Pages/Students/Create.cshtml.cs
следующим кодом:
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
В приведенном выше коде создается объект Student, после чего опубликованные поля формы используются для обновления свойств этого объекта. Метод TryUpdateModelAsync выполняет указанные ниже действия.
- Использует опубликованные значения формы из PageContext свойства в объекте PageModel.
- Обновляет только перечисленные свойства (
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate
). - Ищет поля формы с префиксом "student". Например,
Student.FirstMidName
. Задается без учета регистра символов. - Использует систему привязки модели для преобразования значений формы из строк в типы модели
Student
. Например,EnrollmentDate
преобразуется вDateTime
.
Запустите приложение и создайте сущность учащегося для тестирования страницы создания.
Чрезмерная передача данных
В целях повышения безопасности рекомендуется использовать TryUpdateModel
для обновления полей на основе отправленных значений, поскольку в этом случае исключается чрезмерная передача данных. Например, сущность Student включает свойство Secret
, которое веб-страница не должна обновлять или добавлять:
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; }
}
Даже если приложение не имеет поля Secret
на странице создания или обновления Razor, злоумышленник может установить значение Secret
посредством чрезмерной передачи данных. Злоумышленник может использовать такие средства, как Fiddler, или собственный код JavaScript для отправки значения формы Secret
. В исходном коде не ограничиваются поля, которые используются при создании экземпляра Student связывателем модели.
Какое бы значение ни задал злоумышленник для поля формы Secret
, оно будет обновлено в базе данных. На следующем рисунке показано средство Fiddler, с помощью которого в отправленные значения формы добавляется поле Secret
со значением "OverPost".
Значение "OverPost" успешно добавлено в свойство Secret
вставленной строки. Это происходит несмотря на то, что разработчик приложения не планировал, что свойство Secret
будет устанавливаться на странице создания.
Просмотреть подробности
Модели представления реализуют альтернативный подход к защите от чрезмерной передачи данных.
Модель приложения часто называют моделью домена. Модель предметной области обычно содержит все свойства, необходимые для соответствующей сущности в базе данных. Модель представления содержит только те свойства, которые необходимы для страницы пользовательского интерфейса, например страницы Create.
Помимо модели представления в некоторых приложениях используется модель привязки или модель ввода для передачи данных между классом модели страницы Razor Pages и браузером.
Рассмотрим следующую модель представления StudentVM
:
public class StudentVM
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
В следующем коде модель представления используется StudentVM
для создания нового учащегося:
[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");
}
Метод SetValues устанавливает значения этого объекта, считывая значения из другого объекта PropertyValues. SetValues
использует сопоставление имен свойств. Тип модели представления:
- не обязательно должен быть связан с типом модели;
- должен иметь соответствующие свойства.
При использовании StudentVM
требуется, чтобы страница Create использовала StudentVM
, а не 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");}
}
Обновление страницы Edit
В файле Pages/Students/Edit.cshtml.cs
замените методы OnGetAsync
и OnPostAsync
приведенным ниже кодом.
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();
}
Изменения в коде аналогичны странице Create за некоторыми исключениями:
FirstOrDefaultAsync
заменен на FindAsync; Если включать связанные данные не требуется, более эффективным будет методFindAsync
.OnPostAsync
имеет параметрid
.- Текущий учащийся извлекается из базы данных вместо того, чтобы создавать нового учащегося.
Запустите приложение и протестируйте его, создав и изменив учащегося.
Состояния сущностей
Контекст базы данных отслеживает синхронизацию сущностей в памяти с соответствующими им строками в базе данных. Данные отслеживания определяют поведение при вызове метода SaveChangesAsync. Например, при передаче новой сущности в метод AddAsync ей присваивается состояние Added. При вызове метода SaveChangesAsync
контекст базы данных выполняет команду SQL INSERT
.
Возможны следующие состояния сущности:
Added
: сущность еще не существует в базе данных. МетодSaveChanges
выполняет инструкциюINSERT
.Unchanged
: никакие изменения сущности не сохраняются. Сущность находится в этом состоянии при считывании из базы данных.Modified
: были изменены значения некоторых или всех свойств сущности. МетодSaveChanges
выполняет инструкциюUPDATE
.Deleted
: сущность отмечена для удаления. МетодSaveChanges
выполняет инструкциюDELETE
.Detached
: сущность не отслеживается контекстом базы данных.
В классическом приложении изменения состояния обычно осуществляются автоматически. После считывания сущности и ее изменения ей автоматически присваивается состояние Modified
. При вызове метода SaveChanges
создается инструкция SQL UPDATE
, которая обновляет только измененные свойства.
В веб-приложении объект DbContext
, который считывает сущность и отображает ее данные, ликвидируется после отрисовки страницы. При вызове метода страницы OnPostAsync
выполняется новый веб-запрос с новым экземпляром DbContext
. Если повторно считать сущность в этот новый контекст, будет смоделирована обработка в классическом приложении.
Обновление страницы "Delete" (Удаление)
В этом разделе реализуется пользовательское сообщение об ошибке на случай сбоя при вызове SaveChanges
.
Замените код в Pages/Students/Delete.cshtml.cs
следующим:
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 });
}
}
}
}
Предыдущий код:
- Добавляет возможность ведения журнала.
- Добавляет необязательный параметр
saveChangesError
в сигнатуру методаOnGetAsync
.saveChangesError
указывает, был ли метод вызван после того, как произошел сбой при удалении объекта учащегося.
Операция удаления может завершиться сбоем из-за временных проблем с сетью. Вероятность возникновения временных проблем с сетью выше, когда база данных размещается в облаке. Параметр saveChangesError
имеет значение false
при вызове метода OnGetAsync
страницы удаления из пользовательского интерфейса. Если OnGetAsync
вызывается методом OnPostAsync
из-за сбоя операции удаления, параметру saveChangesError
присваивается значение true
.
Метод OnPostAsync
извлекает выбранную сущность и вызывает метод Remove, чтобы присвоить ей состояние Deleted
. При вызове метода SaveChanges
создается инструкция SQL DELETE
. В случае сбоя Remove
:
- Возникает исключение базы данных.
- Вызывается метод
OnGetAsync
страницы Delete с параметромsaveChangesError=true
.
Добавьте сообщение об ошибке в 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>
Запустите приложение и удалите учащегося, чтобы протестировать страницу удаления.
Следующие шаги
В этом учебнике описывается проверка и настройка шаблонного кода операций CRUD (создание, чтение, обновление и удаление).
Репозиторий отсутствует.
Некоторые разработчики используют уровень служб или шаблон репозитория, чтобы создать уровень абстракции между пользовательским интерфейсом (Razor Pages) и уровнем доступа к данным. В данном учебнике этого не делается. Чтобы свести к минимуму сложность и сосредоточиться на руководстве EF Core, EF Core код добавляется непосредственно в классы модели страницы.
Обновление страницы Details (сведения)
Шаблонный код для страниц учащихся не включает в себя сведения о регистрации. В этом разделе показано, как регистрации добавляются на страницу Details
.
Считывание сведений о регистрации
Чтобы отобразить сведения о регистрации учащегося на странице, эти сведения необходимо считать. Шаблонный код в файле Pages/Students/Details.cshtml.cs
считывает только сведения Student
, но не сведения 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();
}
Замените метод OnGetAsync
приведенным ниже кодом для считывания сведений о регистрации для выбранного учащегося. Изменения выделены.
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();
}
Методы Include и ThenInclude инструктируют контекст для загрузки свойства навигации Student.Enrollments
, а также свойства навигации Enrollment.Course
в пределах каждой регистрации. Эти методы более подробно рассматриваются в руководстве, посвященном чтению связанных данных.
Метод AsNoTracking повышает производительность в сценариях, когда возвращаемые сущности не обновляются в текущем контексте. AsNoTracking
рассматривается позднее в этом учебнике.
Отображение регистраций
Замените код в Pages/Students/Details.cshtml
следующем коде, чтобы отобразить список регистраций. Изменения выделены.
@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>
Приведенный выше код циклически обрабатывает сущности в свойстве навигации Enrollments
. Для каждой регистрации он отображает название курса и оценку. Название курса извлекается из сущности Course
, которая хранится в свойстве навигации Course
сущности Enrollments.
Запустите приложение, выберите вкладку Students (Учащиеся) и щелкните ссылку Details (Сведения) для учащегося. Отобразится список курсов и оценок для выбранного учащегося.
Способы чтения одной сущности
В созданном коде для чтения одной сущности используется метод FirstOrDefaultAsync. Этот метод возвращает значение NULL, если ничего не найдено. В противном случае возвращается первая найденная строка, удовлетворяющая условиям фильтра запросов. FirstOrDefaultAsync
обычно предпочтительнее, чем следующие варианты:
- SingleOrDefaultAsync — вызывает исключение при наличии нескольких сущностей, соответствующих фильтру запросов. Для определения того, может ли запрос вернуть более одной строки,
SingleOrDefaultAsync
пытается получить несколько строк. Это дополнительное действие не требуется, если запрос может вернуть только одну сущность, как при поиске по уникальному ключу. - FindAsync — находит сущность с первичным ключом. Если сущность с первичным ключом отслеживается контекстом, она возвращается без запроса к базе данных. Этот метод оптимизирован для поиска одной сущности, однако вызвать
Include
сFindAsync
невозможно. Поэтому если требуются связанные данные, лучше использоватьFirstOrDefaultAsync
.
Данные маршрута или строка запроса
URL-адрес страницы сведений — https://localhost:<port>/Students/Details?id=1
. Значение первичного ключа сущности содержится в строке запроса. Некоторые разработчики предпочитают передавать значение ключа в данных маршрута: https://localhost:<port>/Students/Details/1
. Дополнительные сведения см. в разделе Обновление созданного кода.
Обновление страницы Create
Шаблонный код OnPostAsync
для страницы создания уязвим к чрезмерной передаче данных. Замените метод OnPostAsync
в файле Pages/Students/Create.cshtml.cs
следующим кодом:
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
В приведенном выше коде создается объект Student, после чего опубликованные поля формы используются для обновления свойств этого объекта. Метод TryUpdateModelAsync выполняет указанные ниже действия.
- Использует опубликованные значения формы из PageContext свойства в объекте PageModel.
- Обновляет только перечисленные свойства (
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate
). - Ищет поля формы с префиксом "student". Например,
Student.FirstMidName
. Задается без учета регистра символов. - Использует систему привязки модели для преобразования значений формы из строк в типы модели
Student
. Например,EnrollmentDate
преобразуется вDateTime
.
Запустите приложение и создайте сущность учащегося для тестирования страницы создания.
Чрезмерная передача данных
В целях повышения безопасности рекомендуется использовать TryUpdateModel
для обновления полей на основе отправленных значений, поскольку в этом случае исключается чрезмерная передача данных. Например, сущность Student включает свойство Secret
, которое веб-страница не должна обновлять или добавлять:
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; }
}
Даже если приложение не имеет поля Secret
на странице создания или обновления Razor, злоумышленник может установить значение Secret
посредством чрезмерной передачи данных. Злоумышленник может использовать такие средства, как Fiddler, или собственный код JavaScript для отправки значения формы Secret
. В исходном коде не ограничиваются поля, которые используются при создании экземпляра Student связывателем модели.
Какое бы значение ни задал злоумышленник для поля формы Secret
, оно будет обновлено в базе данных. На следующем рисунке показано средство Fiddler, с помощью которого в отправленные значения формы добавляется поле Secret
со значением "OverPost".
Значение "OverPost" успешно добавлено в свойство Secret
вставленной строки. Это происходит несмотря на то, что разработчик приложения не планировал, что свойство Secret
будет устанавливаться на странице создания.
Просмотреть подробности
Модели представления реализуют альтернативный подход к защите от чрезмерной передачи данных.
Модель приложения часто называют моделью домена. Модель предметной области обычно содержит все свойства, необходимые для соответствующей сущности в базе данных. Модель представления содержит только те свойства, которые необходимы для страницы пользовательского интерфейса, например страницы Create.
Помимо модели представления в некоторых приложениях используется модель привязки или модель ввода для передачи данных между классом модели страницы Razor Pages и браузером.
Рассмотрим следующую модель представления StudentVM
:
public class StudentVM
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
В следующем коде модель представления используется StudentVM
для создания нового учащегося:
[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");
}
Метод SetValues устанавливает значения этого объекта, считывая значения из другого объекта PropertyValues. SetValues
использует сопоставление имен свойств. Тип модели представления:
- не обязательно должен быть связан с типом модели;
- должен иметь соответствующие свойства.
При использовании StudentVM
требуется, чтобы страница Create использовала StudentVM
, а не 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");}
}
Обновление страницы Edit
В файле Pages/Students/Edit.cshtml.cs
замените методы OnGetAsync
и OnPostAsync
приведенным ниже кодом.
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();
}
Изменения в коде аналогичны странице Create за некоторыми исключениями:
FirstOrDefaultAsync
заменен на FindAsync; Если включать связанные данные не требуется, более эффективным будет методFindAsync
.OnPostAsync
имеет параметрid
.- Текущий учащийся извлекается из базы данных вместо того, чтобы создавать нового учащегося.
Запустите приложение и протестируйте его, создав и изменив учащегося.
Состояния сущностей
Контекст базы данных отслеживает синхронизацию сущностей в памяти с соответствующими им строками в базе данных. Данные отслеживания определяют поведение при вызове метода SaveChangesAsync. Например, при передаче новой сущности в метод AddAsync ей присваивается состояние Added. При вызове метода SaveChangesAsync
контекст базы данных выполняет команду SQL INSERT
.
Возможны следующие состояния сущности:
Added
: сущность еще не существует в базе данных. МетодSaveChanges
выполняет инструкциюINSERT
.Unchanged
: никакие изменения сущности не сохраняются. Сущность находится в этом состоянии при считывании из базы данных.Modified
: были изменены значения некоторых или всех свойств сущности. МетодSaveChanges
выполняет инструкциюUPDATE
.Deleted
: сущность отмечена для удаления. МетодSaveChanges
выполняет инструкциюDELETE
.Detached
: сущность не отслеживается контекстом базы данных.
В классическом приложении изменения состояния обычно осуществляются автоматически. После считывания сущности и ее изменения ей автоматически присваивается состояние Modified
. При вызове метода SaveChanges
создается инструкция SQL UPDATE
, которая обновляет только измененные свойства.
В веб-приложении объект DbContext
, который считывает сущность и отображает ее данные, ликвидируется после отрисовки страницы. При вызове метода страницы OnPostAsync
выполняется новый веб-запрос с новым экземпляром DbContext
. Если повторно считать сущность в этот новый контекст, будет смоделирована обработка в классическом приложении.
Обновление страницы "Delete" (Удаление)
В этом разделе реализуется пользовательское сообщение об ошибке на случай сбоя при вызове SaveChanges
.
Замените код в Pages/Students/Delete.cshtml.cs
следующим:
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 });
}
}
}
}
Предыдущий код:
- Добавляет возможность ведения журнала.
- Добавляет необязательный параметр
saveChangesError
в сигнатуру методаOnGetAsync
.saveChangesError
указывает, был ли метод вызван после того, как произошел сбой при удалении объекта учащегося.
Операция удаления может завершиться сбоем из-за временных проблем с сетью. Вероятность возникновения временных проблем с сетью выше, когда база данных размещается в облаке. Параметр saveChangesError
имеет значение false
при вызове метода OnGetAsync
страницы удаления из пользовательского интерфейса. Если OnGetAsync
вызывается методом OnPostAsync
из-за сбоя операции удаления, параметру saveChangesError
присваивается значение true
.
Метод OnPostAsync
извлекает выбранную сущность и вызывает метод Remove, чтобы присвоить ей состояние Deleted
. При вызове метода SaveChanges
создается инструкция SQL DELETE
. В случае сбоя Remove
:
- Возникает исключение базы данных.
- Вызывается метод
OnGetAsync
страницы Delete с параметромsaveChangesError=true
.
Добавьте сообщение об ошибке в 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>
Запустите приложение и удалите учащегося, чтобы протестировать страницу удаления.
Следующие шаги
В этом учебнике описывается проверка и настройка шаблонного кода операций CRUD (создание, чтение, обновление и удаление).
Репозиторий отсутствует.
Некоторые разработчики используют уровень служб или шаблон репозитория, чтобы создать уровень абстракции между пользовательским интерфейсом (Razor Pages) и уровнем доступа к данным. В данном учебнике этого не делается. Чтобы свести к минимуму сложность и сосредоточиться на руководстве EF Core, EF Core код добавляется непосредственно в классы модели страницы.
Обновление страницы Details (сведения)
Шаблонный код для страниц учащихся не включает в себя сведения о регистрации. В этом разделе регистрации добавляются на страницу сведений.
Считывание сведений о регистрации
Чтобы отобразить сведения о регистрации учащегося на странице, необходимо считать их. Шаблонный код в Pages/Students/Details.cshtml.cs
коде считывает только данные student без данных регистрации:
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();
}
Замените метод OnGetAsync
приведенным ниже кодом для считывания сведений о регистрации для выбранного учащегося. Изменения выделены.
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();
}
Методы Include и ThenInclude инструктируют контекст для загрузки свойства навигации Student.Enrollments
, а также свойства навигации Enrollment.Course
в пределах каждой регистрации. Эти методы более подробно рассматриваются в учебнике, посвященном чтению связанных данных.
Метод AsNoTracking повышает производительность в сценариях, когда возвращаемые сущности не обновляются в текущем контексте. AsNoTracking
рассматривается позднее в этом учебнике.
Отображение регистраций
Замените код в Pages/Students/Details.cshtml
следующем коде, чтобы отобразить список регистраций. Изменения выделены.
@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>
Приведенный выше код циклически обрабатывает сущности в свойстве навигации Enrollments
. Для каждой регистрации он отображает название курса и оценку. Название курса извлекается из сущности Course, которая хранится в свойстве навигации Course
сущности Enrollments.
Запустите приложение, выберите вкладку Students (Учащиеся) и щелкните ссылку Details (Сведения) для учащегося. Отобразится список курсов и оценок для выбранного учащегося.
Способы чтения одной сущности
В созданном коде для чтения одной сущности используется метод FirstOrDefaultAsync. Этот метод возвращает значение NULL, если ничего не найдено. В противном случае возвращается первая найденная строка, удовлетворяющая условиям фильтра запросов. FirstOrDefaultAsync
обычно предпочтительнее, чем следующие варианты:
- SingleOrDefaultAsync — вызывает исключение при наличии нескольких сущностей, соответствующих фильтру запросов. Для определения того, может ли запрос вернуть более одной строки,
SingleOrDefaultAsync
пытается получить несколько строк. Это дополнительное действие не требуется, если запрос может вернуть только одну сущность, как при поиске по уникальному ключу. - FindAsync — находит сущность с первичным ключом. Если сущность с первичным ключом отслеживается контекстом, она возвращается без запроса к базе данных. Этот метод оптимизирован для поиска одной сущности, однако вызвать
Include
сFindAsync
невозможно. Поэтому если требуются связанные данные, лучше использоватьFirstOrDefaultAsync
.
Данные маршрута или строка запроса
URL-адрес страницы сведений — https://localhost:<port>/Students/Details?id=1
. Значение первичного ключа сущности содержится в строке запроса. Некоторые разработчики предпочитают передавать значение ключа в данных маршрута: https://localhost:<port>/Students/Details/1
. Дополнительные сведения см. в разделе Обновление созданного кода.
Обновление страницы Create
Шаблонный код OnPostAsync
для страницы создания уязвим к чрезмерной передаче данных. Замените метод OnPostAsync
в файле Pages/Students/Create.cshtml.cs
следующим кодом:
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
В приведенном выше коде создается объект Student, после чего опубликованные поля формы используются для обновления свойств этого объекта. Метод TryUpdateModelAsync выполняет указанные ниже действия.
- Использует опубликованные значения формы из PageContext свойства в объекте PageModel.
- Обновляет только перечисленные свойства (
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate
). - Ищет поля формы с префиксом "student". Например,
Student.FirstMidName
. Задается без учета регистра символов. - Использует систему привязки модели для преобразования значений формы из строк в типы модели
Student
. Например,EnrollmentDate
следует преобразовать в тип DateTime.
Запустите приложение и создайте сущность учащегося для тестирования страницы создания.
Чрезмерная передача данных
В целях повышения безопасности рекомендуется использовать TryUpdateModel
для обновления полей на основе отправленных значений, поскольку в этом случае исключается чрезмерная передача данных. Например, сущность Student включает свойство Secret
, которое веб-страница не должна обновлять или добавлять:
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; }
}
Даже если приложение не имеет поля Secret
на странице создания или обновления Razor, злоумышленник может установить значение Secret
посредством чрезмерной передачи данных. Злоумышленник может использовать такие средства, как Fiddler, или собственный код JavaScript для отправки значения формы Secret
. В исходном коде не ограничиваются поля, которые используются при создании экземпляра Student связывателем модели.
Какое бы значение ни задал злоумышленник для поля формы Secret
, оно будет обновлено в базе данных. На следующем рисунке показано средство Fiddler, с помощью которого в отправленные значения формы добавляется поле Secret
(со значением "OverPost").
Значение "OverPost" успешно добавлено в свойство Secret
вставленной строки. Это происходит несмотря на то, что разработчик приложения не планировал, что свойство Secret
будет устанавливаться на странице создания.
Просмотреть подробности
Модели представления реализуют альтернативный подход к защите от чрезмерной передачи данных.
Модель приложения часто называют моделью домена. Модель предметной области обычно содержит все свойства, необходимые для соответствующей сущности в базе данных. Модель представления содержит только те свойства, которые необходимы соответствующему пользовательскому интерфейсу (например, странице создания).
Помимо модели представления в некоторых приложениях используется модель привязки или модель ввода для передачи данных между классом модели страницы Razor Pages и браузером.
Рассмотрим следующую модель представления Student
:
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; }
}
}
В следующем коде модель представления используется StudentVM
для создания нового учащегося:
[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");
}
Метод SetValues устанавливает значения этого объекта, считывая значения из другого объекта PropertyValues. SetValues
использует сопоставление имен свойств. Тип модели представления может быть не связан с типом модели, однако они должны содержать совпадающие свойства.
При использовании StudentVM
необходимо обновить Create.cshtml, чтобы использовать StudentVM
вместо Student
.
Обновление страницы Edit
В файле Pages/Students/Edit.cshtml.cs
замените методы OnGetAsync
и OnPostAsync
приведенным ниже кодом.
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();
}
Изменения в коде аналогичны странице Create за некоторыми исключениями:
FirstOrDefaultAsync
заменен на FindAsync; Если включать связанные данные не требуется, более эффективным будет методFindAsync
.OnPostAsync
имеет параметрid
.- Текущий учащийся извлекается из базы данных вместо того, чтобы создавать нового учащегося.
Запустите приложение и протестируйте его, создав и изменив учащегося.
Состояния сущностей
Контекст базы данных отслеживает синхронизацию сущностей в памяти с соответствующими им строками в базе данных. Данные отслеживания определяют поведение при вызове метода SaveChangesAsync. Например, при передаче новой сущности в метод AddAsync ей присваивается состояние Added. При вызове метода SaveChangesAsync
контекст базы данных выполняет команду SQL INSERT.
Возможны следующие состояния сущности:
Added
: сущность еще не существует в базе данных. МетодSaveChanges
выполняет инструкцию INSERT.Unchanged
: никакие изменения сущности не сохраняются. Сущность находится в этом состоянии при считывании из базы данных.Modified
: были изменены значения некоторых или всех свойств сущности. МетодSaveChanges
выполняет инструкцию UPDATE.Deleted
: сущность отмечена для удаления. МетодSaveChanges
выполняет инструкцию DELETE.Detached
: сущность не отслеживается контекстом базы данных.
В классическом приложении изменения состояния обычно осуществляются автоматически. После считывания сущности и ее изменения ей автоматически присваивается состояние Modified
. При вызове метода SaveChanges
создается инструкция SQL UPDATE, которая обновляет только измененные свойства.
В веб-приложении объект DbContext
, который считывает сущность и отображает ее данные, ликвидируется после отрисовки страницы. При вызове метода страницы OnPostAsync
выполняется новый веб-запрос с новым экземпляром DbContext
. Если повторно считать сущность в этот новый контекст, будет смоделирована обработка в классическом приложении.
Обновление страницы "Delete" (Удаление)
В этом разделе реализуется пользовательское сообщение об ошибке на случай сбоя при вызове SaveChanges
.
Замените код в Pages/Students/Delete.cshtml.cs
следующим: Изменения выделены (кроме очистки инструкций 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 });
}
}
}
}
В предыдущем коде в сигнатуру метода OnGetAsync
добавляется необязательный параметр saveChangesError
. saveChangesError
указывает, был ли метод вызван после того, как произошел сбой при удалении объекта учащегося. Операция удаления может завершиться сбоем из-за временных проблем с сетью. Вероятность возникновения временных проблем с сетью выше, когда база данных размещается в облаке. Параметр saveChangesError
имеет значение false при вызове метода OnGetAsync
страницы удаления из пользовательского интерфейса. Если OnGetAsync
вызывается методом OnPostAsync
(из-за сбоя операции удаления), параметру saveChangesError
присваивается значение true.
Метод OnPostAsync
извлекает выбранную сущность и вызывает метод Remove, чтобы присвоить ей состояние Deleted
. При вызове метода SaveChanges
создается инструкция SQL DELETE. В случае сбоя Remove
:
- Возникает исключение базы данных.
- Вызывается метод
OnGetAsync
страницы Delete с параметромsaveChangesError=true
.
Добавьте сообщение об ошибке на страницу удаления Razor (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>
Запустите приложение и удалите учащегося, чтобы протестировать страницу удаления.
Следующие шаги
ASP.NET Core