Nguon: Microsoft Learn · .NET 8.0

Phần 2: Razor Pages với EF Core trong ASP.NET Core - CRUD

Nguồn: Part 2: Razor Pages - EF Core CRUD

Bởi Tom Dykstra, Jeremy Likness, và Jon P Smith

Ứng dụng web Contoso University minh họa cách tạo ứng dụng web Razor Pages (trang Razor) sử dụng EF Core (Entity Framework Core) và Visual Studio. Để biết thêm thông tin về series hướng dẫn này, xem hướng dẫn đầu tiên.

Nếu gặp vấn đề không giải quyết được, hãy tải ứng dụng hoàn chỉnh và so sánh code đó với những gì bạn đã tạo khi làm theo hướng dẫn.

Bài này trình bày hướng dẫn thứ hai trong series tám phần. Trong hướng dẫn này, bạn xem xét và tùy chỉnh code CRUD (create - tạo, read - đọc, update - cập nhật, delete - xóa) mà scaffolding (công cụ tạo code tự động) tạo ra cho các trang Razor.

Trong hướng dẫn này, bạn:

Điều kiện tiên quyết

Xem xét cách tiếp cận trong hướng dẫn (không dùng repository)

Một số lập trình viên sử dụng service layer (tầng dịch vụ) hoặc repository pattern (mẫu repository) để tạo lớp trừu tượng giữa UI (giao diện người dùng - Razor Pages) và data access layer (tầng truy cập dữ liệu). Thay vì cách đó, hướng dẫn này thêm code EF Core trực tiếp vào các lớp page model. Phương pháp này giúp giảm thiểu sự phức tạp và giữ cho hướng dẫn tập trung vào EF Core.

Cập nhật trang Details

Code scaffolded cho các trang Students không bao gồm dữ liệu enrollment (đăng ký). Trong phần này, enrollments được thêm vào trang Details.

Đọc enrollments

Để hiển thị dữ liệu enrollment của sinh viên trên trang, dữ liệu enrollment phải được đọc. Code scaffolded trong file Pages/Students/Details.cshtml.cs chỉ đọc dữ liệu Student mà không có dữ liệu Enrollment:

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

Thay thế phương thức OnGetAsync bằng code sau để đọc dữ liệu enrollment cho sinh viên được chọn:

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

Các phương thức IncludeThenInclude khiến context tải navigation property (thuộc tính điều hướng) Student.Enrollments, và trong mỗi enrollment, tải navigation property Enrollment.Course. Các phương thức này được xem xét chi tiết trong Đọc dữ liệu liên quan (Hướng dẫn 6/8).

Phương thức AsNoTracking cải thiện hiệu năng trong các tình huống mà các entity được trả về không được cập nhật trong context hiện tại. AsNoTracking được thảo luận ở phần sau trong hướng dẫn này.

Hiển thị enrollments

Để hiển thị danh sách enrollment, thay thế code trong file Pages/Students/Details.cshtml bằng code sau:

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>

Code này lặp qua các entity trong navigation property Enrollments. Với mỗi enrollment, code hiển thị tiêu đề khóa học và điểm. Tiêu đề khóa học được lấy từ entity Course được lưu trong navigation property Course của entity Enrollments.

Chạy ứng dụng, chọn tab Students, và chọn link Details cho một sinh viên. Danh sách các khóa học và điểm của sinh viên được chọn sẽ hiển thị.

Sử dụng các phương thức khác nhau để đọc một entity

Code được tạo sử dụng phương thức FirstOrDefaultAsync để đọc một entity. Phương thức này trả về null nếu không tìm thấy gì. Nếu không, nó trả về hàng đầu tiên thỏa mãn tiêu chí lọc của truy vấn. Phương thức FirstOrDefaultAsync thường là lựa chọn tốt hơn so với các phương án thay thế sau:

Route data vs. query string

URL cho trang Detailshttps://localhost:<port>/Students/Details?id=1. Giá trị primary key của entity nằm trong query string (chuỗi truy vấn). Một số lập trình viên thích truyền giá trị khóa trong route data (dữ liệu tuyến đường): https://localhost:<port>/Students/Details/1.

Cập nhật trang Create

Code OnPostAsync scaffolded cho trang Create dễ bị tấn công overposting. Thay thế phương thức OnPostAsync trong file Pages/Students/Create.cshtml.cs bằng code sau:

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

Sử dụng phương thức TryUpdateModelAsync

Code trong phần trên tạo một đối tượng Student rồi sử dụng các trường form đã đăng để cập nhật các thuộc tính của đối tượng Student. Phương thức TryUpdateModelAsync:

Chạy ứng dụng và tạo một entity sinh viên để kiểm tra trang Create.

Ngăn chặn overposting

Sử dụng phương thức TryUpdateModel để cập nhật các trường với giá trị đã đăng là best practice (thực hành tốt nhất) về bảo mật vì nó ngăn chặn overposting. Ví dụ, giả sử entity Student bao gồm thuộc tính Secret mà trang web này không nên cập nhật hoặc thêm:

csharp
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; }
}

Ngay cả khi ứng dụng không có trường Secret trên trang Create hoặc Update, hacker có thể đặt giá trị Secret bằng cách overposting. Hacker có thể sử dụng một công cụ như Fiddler, hoặc viết một số JavaScript, để đăng giá trị form Secret. Code gốc không giới hạn các trường mà model binder sử dụng khi tạo instance Student.

Bất kỳ giá trị nào hacker chỉ định cho trường form Secret sẽ được cập nhật trong database.

Làm việc với View model

View model (mô hình hiển thị) cung cấp một cách thay thế để ngăn chặn overposting.

Application model (mô hình ứng dụng) thường được gọi là domain model (mô hình miền). Domain model thường chứa tất cả các thuộc tính cần thiết cho entity tương ứng trong database. View model chỉ chứa các thuộc tính cần thiết cho trang UI, ví dụ trang Create.

Ngoài view model, một số ứng dụng sử dụng binding model (mô hình ràng buộc) hoặc input model (mô hình đầu vào) để truyền dữ liệu giữa lớp page model của Razor Pages và trình duyệt.

Xem xét view model StudentVM sau:

csharp
public class StudentVM
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
}

Code sau sử dụng view model StudentVM để tạo sinh viên mới:

csharp
[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");
}

Phương thức SetValues đặt các giá trị của đối tượng này bằng cách đọc giá trị từ một đối tượng PropertyValues khác. Phương thức SetValues sử dụng khớp tên thuộc tính. Kiểu view model:

Sử dụng view model StudentVM yêu cầu trang Create sử dụng entity StudentVM thay vì entity Student.

Cập nhật trang Edit

Trong file Pages/Students/Edit.cshtml.cs, thay thế các phương thức OnGetAsyncOnPostAsync bằng code sau:

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

Các thay đổi trong code tương tự trang Create với một vài ngoại lệ:

Chạy ứng dụng và kiểm tra bằng cách tạo và sửa một sinh viên.

Sử dụng Entity States (trạng thái entity)

Database context theo dõi xem các entity trong bộ nhớ có đồng bộ với các hàng tương ứng trong database không. Thông tin theo dõi này xác định những gì xảy ra khi phương thức SaveChangesAsync được gọi. Ví dụ, khi một entity mới được truyền vào phương thức AddAsync, trạng thái của entity đó được đặt thành Added. Khi phương thức SaveChangesAsync được gọi, database context phát ra lệnh SQL INSERT.

Một entity có thể ở một trong các trạng thái sau:

Trong ứng dụng desktop, thay đổi trạng thái thường được thiết lập tự động. Trong ứng dụng web, đối tượng DbContext đọc entity và hiển thị dữ liệu sẽ bị dispose (giải phóng) sau khi trang được render. Khi phương thức OnPostAsync của trang được gọi, một request web mới được thực hiện với một instance mới của đối tượng DbContext. Đọc lại entity trong context mới mô phỏng xử lý desktop.

Cập nhật trang Delete

Trong phần này, thông báo lỗi tùy chỉnh được triển khai khi lệnh gọi SaveChanges thất bại.

Thay thế code trong file Pages/Students/Delete.cshtml.cs bằng code sau:

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

Code này thêm các chức năng sau:

Thao tác xóa có thể thất bại vì các vấn đề mạng tạm thời. Lỗi mạng tạm thời có nhiều khả năng xảy ra hơn khi database ở trên cloud. Tham số saveChangesErrorfalse khi phương thức OnGetAsync của trang Delete được gọi từ UI. Khi phương thức OnGetAsync được gọi bởi phương thức OnPostAsync vì thao tác xóa thất bại, tham số saveChangesErrortrue.

Phương thức OnPostAsync lấy entity được chọn, sau đó gọi phương thức Remove để đặt trạng thái entity thành Deleted. Khi SaveChanges được gọi, lệnh SQL DELETE được tạo ra. Nếu lệnh gọi Remove thất bại:

Thêm thông báo lỗi vào trang Delete (Pages/Students/Delete.cshtml):

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>

Chạy ứng dụng và xóa một sinh viên để kiểm tra trang Delete.