Руководство 2. Razor Страницы с EF Core в ASP.NET Core — CRUD

Примечание.

Это не последняя версия этой статьи. В текущей версии см. версию .NET 10 этой статьи.

Предупреждение

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущей версии см. версию .NET 10 этой статьи.

Авторы: Том Дайкстра (Tom Dykstra), Джереми Ликнесс (Jeremy Likness) и Йон П. Смит (Jon P Smith)

Веб-приложение Contoso University демонстрирует создание Razor веб-приложений Pages с помощью EF Core Visual Studio. Сведения о серии руководств см. в первом руководстве серии.

При возникновении проблем, которые вам не удается устранить, скачайте готовое приложение и сравните его код с тем, который вы создали в процессе работы с этим руководством.

В этой статье представлено второе руководство из восьми частей серии. В этом руководстве вы сначала просмотрите, а затем настроите код CRUD (создание, чтение, обновление, удаление), который каркас автоматически создает для страниц Razor.

Изучив это руководство, вы:

  • Настройка страниц: сведения, создание, изменение и удаление
  • Узнайте, как защититься от избыточной публикации
  • Обзор различных методов чтения одной сущности
  • Изучение модели представления

Необходимые условия

Ознакомьтесь с обучающим подходом (без репозитория)

Некоторые разработчики используют слой служб или шаблон репозитория для создания слоя абстракции между пользовательским интерфейсом ( Razor Pages) и уровнем доступа к данным. В этом руководстве, вместо вышеупомянутого подхода, код EF Core добавляется непосредственно в классы модели страницы. Этот метод помогает свести к минимуму сложность и сосредоточиться на EF Core руководстве.

Обновление страницы 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 свойства навигации, и в пределах каждой записи свойства навигации. Эти методы подробно рассматриваются в разделе "Чтение связанных данных" (учебник 6 из 8).

Метод 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.

Запустите приложение, перейдите на вкладку "Учащиеся " и выберите ссылку "Сведения " для учащегося. Отобразится список курсов и оценок для выбранного учащегося.

Использование различных методов для чтения одной сущности

Созданный код использует метод FirstOrDefaultAsync для чтения одной сущности. Этот метод возвращает значение NULL, если ничего не найдено. В противном случае он возвращает первую строку, которая удовлетворяет критериям фильтра запросов. Как правило, FirstOrDefaultAsync этот метод лучше подходит, чем следующие варианты.

  • Метод SingleOrDefaultAsync создает исключение, если существует несколько сущностей, удовлетворяющих фильтру запросов. Чтобы определить, может ли запрос возвращать несколько строк, SingleOrDefaultAsync метод пытается получить несколько строк. Дополнительная работа не требуется, если запрос может возвращать только одну сущность, как при поиске по уникальному ключу.

  • Метод FindAsync находит сущность с первичным ключом. Если контекст отслеживает сущность с первичным ключом, ключ возвращается без запроса к базе данных. Этот метод оптимизирован для поиска одной сущности, но нельзя вызвать метод Include с помощью метода FindAsync. Если нужны связанные данные, FirstOrDefaultAsync метод лучше подходит.

Данные маршрута или строка запроса

URL-адрес страницы https://localhost:<port>/Students/Details?id=1. Значение первичного ключа сущности содержится в строке запроса. Некоторые разработчики предпочитают передавать значение ключа в данных маршрута: https://localhost:<port>/Students/Details/1. Дополнительные сведения см. в разделе "Обновление созданных страниц" (учебник 5 из 8).

Обновление страницы 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 объект, а затем использует опубликованные поля формы для обновления 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 поля на странице создания или на странице обновления, хакер может задать Secret значение путем сверхпубликации. Хакер может использовать средство, например Fiddler, или написать код JavaScript для публикации Secret значения формы. Исходный код не ограничивает поля, которые использует модельный связующий элемент, когда создается экземпляр Student.

Какое бы значение ни задал злоумышленник для поля формы Secret, оно будет обновлено в базе данных. На следующем рисунке показан инструмент Fiddler, добавляющий Secret поле со значением OverPost в опубликованные значения формы.

Снимок экрана: представление Composer в Fiddler с полем секрета, добавленным в текст запроса.

Значение "OverPost" успешно добавлено в свойство Secret вставленной строки. Этот результат возникает, даже когда разработчик приложения никогда не предполагал задавать Secret свойство на странице Создать.

Работа с моделью представления

Модели представления реализуют альтернативный подход к защите от чрезмерной передачи данных.

Модель приложения часто называется моделью домена. Модель предметной области обычно содержит все свойства, необходимые для соответствующей сущности в базе данных. Модель представления содержит только свойства, необходимые для страницы пользовательского интерфейса, например страницу создания .

Помимо модели представления в некоторых приложениях используется модель привязки или модель ввода для передачи данных между классом модели страницы 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 Для использования модели представления требуется, чтобы страница "Создание" использовала 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");}
}

Рассмотрим следующую модель представления данных 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 замените OnGetAsyncOnPostAsync методы следующим кодом.

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

Изменения кода похожи на страницу создания с несколькими исключениями:

  • Метод 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 объекта. Перечитывание сущности в новом контексте имитирует обработку на настольном компьютере.

Обновить страницу "Удаление"

В этом разделе реализуется пользовательское сообщение об ошибке на случай сбоя при вызове 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 });
            }
        }
    }
}

Этот код добавляет следующие функции:

  • Ведение журнала для .NET и ASP.NET Core.
  • Необязательный параметр saveChangesError в сигнатуре метода OnGetAsync. Параметр saveChangesError указывает, происходит ли вызов метода после сбоя удаления Student объекта.

Замените код в файле 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 });
            }
        }
    }
}

Этот код добавляет необязательный параметр saveChangesError в сигнатуру метода OnGetAsync. Параметр saveChangesError указывает, происходит ли вызов метода после сбоя удаления Student объекта.

Операция удаления может завершиться сбоем из-за временных проблем с сетью. Вероятность возникновения временных проблем с сетью выше, когда база данных размещается в облаке. Параметр saveChangesError становится false при вызове метода Delete страницы OnGetAsync через пользовательский интерфейс. OnGetAsync метод вызывается методом OnPostAsync, если операция удаления завершилась сбоем,saveChangesError параметр имеет значение true.

Метод OnPostAsync извлекает выбранную сущность, а затем вызывает метод Remove , чтобы задать состояние Deletedсущности. При вызове метода SaveChanges создается инструкция SQL DELETE. Если вызов метода Remove завершается ошибкой:

  • Возникает исключение базы данных.
  • Метод Delete page OnGetAsync вызывается с параметром 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>
@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>

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

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