Nguon: Microsoft Learn · .NET 8.0

Triển khai CRUD - ASP.NET MVC với EF Core

> Nguồn: Implement CRUD - ASP.NET MVC with EF Core

Trong hướng dẫn trước, bạn đã tạo ứng dụng MVC lưu trữ và hiển thị dữ liệu bằng cách sử dụng Entity Framework (EF) Core và SQL Server local database. Trong bài này, bạn xem xét và tùy chỉnh code CRUD (create, read, update, delete - tạo, đọc, cập nhật, xóa) mà MVC scaffolding tự động tạo ra trong controllers và views.

Lưu ý: Thực hành phổ biến là triển khai repository pattern (mẫu kho lưu trữ) để tạo lớp trừu tượng giữa controller và lớp truy cập dữ liệu. Để giữ các ví dụ đơn giản và tập trung vào việc trình bày cách sử dụng Entity Framework, các hướng dẫn không sử dụng repositories.

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

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

Tùy chỉnh trang Details

Code được scaffold cho trang Students Index bỏ qua thuộc tính Enrollments vì thuộc tính này chứa một collection. Trang Details hiển thị nội dung của collection dưới dạng bảng HTML.

Trong file Controllers/StudentsController.cs, action method cho Details view sử dụng phương thức FirstOrDefaultAsync để lấy một entity Student duy nhất. Bạn cần thêm code gọi các phương thức Include, ThenIncludeAsNoTracking:

csharp
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var student = await _context.Students
        .Include(s => s.Enrollments)
            .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

    if (student == null)
    {
        return NotFound();
    }

    return View(student);
}

Các phương thức IncludeThenInclude khiến context tải thuộc tính điều hướng Student.Enrollments và thuộc tính điều hướng Enrollment.Course trong mỗi enrollment.

Phương thức AsNoTracking cải thiện hiệu suất trong các tình huống khi các entity được trả về không được cập nhật trong vòng đời của context hiện tại.

Cấu hình route data

Giá trị key được truyền cho phương thức Details đến từ route data (dữ liệu route). Route data là dữ liệu mà model binder tìm thấy trong một đoạn của URL.

csharp
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});

Thêm enrollments vào Details view

Mở file Views/Students/Details.cshtml. Sau trường cuối cùng và ngay trước thẻ đóng </dl>, thêm code sau để hiển thị danh sách enrollments:

cshtml
<dt class="col-sm-2">
    @Html.DisplayNameFor(model => model.Enrollments)
</dt>
<dd class="col-sm-10">
    <table class="table">
        <tr>
            <th>Course Title</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.Enrollments)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Course.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr>
        }
    </table>
</dd>

Cập nhật trang Create

Trong file StudentsController.cs, sửa đổi phương thức HttpPost Create bằng cách thêm khối try-catch và xóa thuộc tính ID khỏi thuộc tính Bind:

csharp
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
    [Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
{
    try
    {
        if (ModelState.IsValid)
        {
            _context.Add(student);
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
    }
    catch (DbUpdateException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.
        ModelState.AddModelError("", "Unable to save changes. " +
            "Try again, and if the problem persists " +
            "see your system administrator.");
    }
    return View(student);
}

Code này thêm entity Student được tạo bởi ASP.NET Core MVC model binder (trình liên kết model) vào entity set Students, sau đó lưu các thay đổi vào database.

Bạn đã xóa thuộc tính ID khỏi thuộc tính Bind vì ID là giá trị khóa chính mà SQL Server đặt tự động khi nó chèn hàng.

Bảo vệ chống overposting

Thuộc tính Bind mà code được scaffold bao gồm trên phương thức Create là một cách để bảo vệ chống overposting trong các tình huống tạo mới. Ví dụ, giả sử entity Student có thuộc tính Secret mà bạn không muốn trang web này đặt.

Ngay cả khi bạn không có trường Secret trên trang web, hacker có thể sử dụng công cụ như Fiddler, hoặc viết JavaScript, để đăng một giá trị form Secret. Không có thuộc tính Bind giới hạn các trường mà model binder sử dụng khi tạo instance Student, model binder có thể lấy giá trị form Secret đó và sử dụng nó để tạo instance entity Student.

Bạn có thể ngăn chặn overposting trong các tình huống chỉnh sửa bằng cách đọc entity từ database trước và sau đó gọi phương thức TryUpdateModel.

Cập nhật trang Edit

Sử dụng code HttpPost Edit được khuyến nghị: Đọc và cập nhật

Thay thế phương thức HttpPost Edit bằng code sau:

csharp
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
    if (id == null)
    {
        return NotFound();
    }
    var studentToUpdate = await _context.Students.FirstOrDefaultAsync(s => s.ID == id);
    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        try
        {
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error (uncomment ex variable name and write a log.)
            ModelState.AddModelError("", "Unable to save changes. " +
                "Try again, and if the problem persists, " +
                "see your system administrator.");
        }
    }
    return View(studentToUpdate);
}

Những thay đổi này triển khai phương pháp bảo mật tốt nhất để ngăn chặn overposting. Code mới đọc entity hiện có và gọi phương thức TryUpdateModel để cập nhật các trường trong entity được lấy dựa trên dữ liệu nhập của người dùng trong dữ liệu form được đăng.

Hiểu trạng thái entity

Database context theo dõi xem các entity trong memory có đồng bộ với các hàng tương ứng trong database hay không. Thông tin này xác định điều gì xảy ra khi bạn gọi phương thức SaveChanges.

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

Cập nhật trang Delete

Thay thế phương thức action HttpGet Delete bằng code sau, quản lý báo cáo lỗi:

csharp
public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)
{
    if (id == null)
    {
        return NotFound();
    }

    var student = await _context.Students
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);
    if (student == null)
    {
        return NotFound();
    }

    if (saveChangesError.GetValueOrDefault())
    {
        ViewData["ErrorMessage"] =
            "Delete failed. Try again, and if the problem persists " +
            "see your system administrator.";
    }

    return View(student);
}

Sử dụng phương pháp read-first cho HttpPost Delete

Thay thế phương thức action HttpPost Delete (có tên DeleteConfirmed) bằng code sau:

csharp
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    var student = await _context.Students.FindAsync(id);
    if (student == null)
    {
        return RedirectToAction(nameof(Index));
    }

    try
    {
        _context.Students.Remove(student);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    catch (DbUpdateException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.)
        return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
    }
}

Code lấy entity được chọn, sau đó gọi phương thức Remove để đặt trạng thái của entity thành Deleted. Khi phương thức SaveChanges được gọi, một lệnh SQL DELETE được tạo ra.

Cập nhật Delete view

Trong file Views/Student/Delete.cshtml, thêm thông báo lỗi giữa tiêu đề h2 và tiêu đề h3:

cshtml
<h2>Delete</h2>
<p class="text-danger">@ViewData["ErrorMessage"]</p>
<h3>Are you sure you want to delete this?</h3>

Đóng kết nối database

Để giải phóng các tài nguyên mà kết nối database chiếm giữ, instance context phải được dispose (hủy) sớm nhất có thể khi bạn hoàn thành. ASP.NET Core built-in dependency injection đảm nhận việc dọn dẹp.

Xử lý transactions (giao dịch)

Mặc định, Entity Framework triển khai transactions một cách ẩn. Trong các tình huống bạn thực hiện thay đổi cho nhiều hàng hoặc bảng và sau đó gọi phương thức SaveChanges, Entity Framework tự động đảm bảo rằng tất cả các thay đổi của bạn thành công hoặc tất cả đều thất bại.

Vô hiệu hóa theo dõi entity objects (no-tracking queries)

Khi database context lấy các hàng bảng và tạo các entity objects đại diện cho chúng, mặc định nó theo dõi xem các entity trong memory có đồng bộ với database hay không. Dữ liệu trong memory hoạt động như cache và được sử dụng khi bạn cập nhật entity.

Bạn có thể vô hiệu hóa theo dõi entity objects trong memory bằng cách gọi phương thức AsNoTracking. Đây là một số tình huống phổ biến cho thao tác này:

Để biết thêm thông tin, xem Tracking versus no-tracking queries.