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:
- Tùy chỉnh trang Details bằng cách thêm dữ liệu enrollment với eager loading
- Cập nhật trang Create với bảo mật và xử lý lỗi
- Cập nhật trang Edit để ngăn chặn overposting
- Cập nhật trang Delete với báo cáo lỗi
- Đóng các kết nối database
Điều kiện tiên quyết
- Hoàn thành hướng dẫn trước, Get started with EF Core in an ASP.NET MVC web app.
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, ThenInclude và AsNoTracking:
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 Include và ThenInclude 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.
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:
<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:
[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:
[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:
Added: Entity chưa tồn tại trong database. Phương thứcSaveChangesphát ra câu lệnhINSERT.Unchanged: Không cần làm gì với entity này bởi phương thứcSaveChanges.Modified: Một số hoặc tất cả các giá trị thuộc tính của entity đã được sửa đổi. Phương thứcSaveChangesphát ra câu lệnhUPDATE.Deleted: Entity được đánh dấu để xóa. Phương thứcSaveChangesphát ra câu lệnhDELETE.Detached: Entity không được database context theo dõi.
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:
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:
[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:
<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:
- Trong vòng đời context, bạn không cần cập nhật bất kỳ entity nào, và bạn không cần EF tự động tải navigation properties với entities được lấy bởi các truy vấn riêng biệt. Các điều kiện này thường được đáp ứng trong các phương thức action HttpGet của controller.
- Bạn đang chạy truy vấn lấy một lượng lớn dữ liệu, và chỉ một phần nhỏ dữ liệu được trả về sẽ được cập nhật.
- Bạn muốn attach một entity để thực hiện cập nhật, nhưng trước đó bạn đã lấy cùng entity đó cho một mục đích khác.
Để biết thêm thông tin, xem Tracking versus no-tracking queries.