Часть 2. Razor Страницы с EF Core ASP.NET Core — CRUD

Авторы: Том Дайкстра (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".

Fiddler adding Secret field

Значение "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".

Fiddler adding Secret field

Значение "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").

Fiddler adding Secret field

Значение "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>

Запустите приложение и удалите учащегося, чтобы протестировать страницу удаления.

Следующие шаги