ASP.NET Core: Razor Pages với Entity Framework Core trong ASP.NET Core - CRUD - Hướng dẫn 2/8
Ứng dụng web của Đại học Contoso trình bày cách tạo các ứng dụng web Razor Pages bằng EF Core và Visual Studio. Để biết thông tin về loạt bài hướng dẫn, hãy xem phần hướng dẫn đầu tiên.
Nếu bạn gặp sự cố mà bạn không thể giải quyết, hãy tải xuống ứng dụng đã hoàn thiện và so sánh mã đó với mã bạn đã tạo bằng cách làm theo hướng dẫn.
Trong hướng dẫn này, mã CRUD (Create, Read, Update, Delete) trên scaffold được xem xét và tùy chỉnh.
Không có kho lưu trữ
Một số nhà phát triển sử dụng lớp dịch vụ hoặc mẫu kho lưu trữ để tạo lớp trừu tượng giữa giao diện người dùng (Razor Pages) và lớp truy cập dữ liệu. Hướng dẫn này không làm điều đó. Để giảm thiểu độ phức tạp và giữ cho hướng dẫn tập trung vào EF Core, mã EF Core được thêm trực tiếp vào các lớp mô hình trang.
Cập nhật trang Details
Mã scaffold cho các trang Students không bao gồm dữ liệu đăng ký. Trong phần này, đăng ký được thêm vào trang Details
.
Đọc các enrollment
Để hiển thị dữ liệu đăng ký của sinh viên trên trang, dữ liệu đăng ký phải được đọc. Mã scaffold Pages/Students/Details.cshtml.cs
chỉ đọc dữ liệu Student
, không có dữ liệu 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();
}
Thay thế phương thức OnGetAsync
bằng đoạn code sau để đọc dữ liệu đăng ký cho sinh viên đã chọn. Những thay đổi được đánh dấu.
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 Include và ThenInclude khiến ngữ cảnh tải property điều hướng Student.Enrollments
và trong mỗi lần đăng ký property điều hướng Enrollment.Course
. Các phương thức này được kiểm tra chi tiết trong hướng dẫn Đọc dữ liệu liên quan.
Phương thức AsNoTracking cải thiện hiệu suất trong các tình huống trong đó các thực thể được trả về không được cập nhật trong ngữ cảnh hiện tại. AsNoTracking
được thảo luận sau trong hướng dẫn này.
Hiển thị enrollment
Thay code trong Pages/Students/Details.cshtml
bằng đoạn code sau đây để hiển thị danh sách đăng ký. Những thay đổi được đánh dấu.
@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>
Đoạn code trên lặp qua các thực thể trong property điều hướng Enrollments
. Đối với mỗi lần đăng ký, nó sẽ hiển thị tên khóa học và điểm. Tiêu đề khóa học được truy xuất từ thực thể Course
được lưu trữ trong property điều hướng Course
của thực thể Enrollment.
Chạy ứng dụng, chọn tab Students và nhấp vào liên kết Details của mỗi sinh viên. Lúc này danh sách các khóa học và điểm cho sinh viên đã chọn được hiển thị.
Các cách để đọc một thực thể
Code được tạo sử dụng FirstOrDefaultAsync để đọc một thực thể. Phương thức này trả về null nếu không tìm thấy gì; ngược lại nó trả về hàng đầu tiên được tìm thấy thỏa mãn tiêu chí bộ lọc truy vấn. FirstOrDefaultAsync
thường là một lựa chọn tốt hơn so với các lựa chọn thay thế sau:
- SingleOrDefaultAsync - Đưa ra một ngoại lệ nếu có nhiều thực thể thỏa mãn bộ lọc truy vấn. Để xác định xem truy vấn có thể trả về nhiều hàng hay không,
SingleOrDefaultAsync
sẽ thử tìm nạp nhiều hàng. Công việc bổ sung này là không cần thiết nếu truy vấn chỉ có thể trả về một thực thể, như khi truy vấn tìm kiếm trên một khóa duy nhất. - FindAsync - Tìm một thực thể có khóa chính (PK). Nếu một thực thể có PK đang được theo dõi bởi ngữ cảnh, nó sẽ được trả về mà không có yêu cầu đối với cơ sở dữ liệu. Phương thức này được tối ưu hóa để tra cứu một thực thể duy nhất, nhưng bạn không thể gọi
Include
bằngFindAsync
. Vì vậy, nếu dữ liệu liên quan là cần thiết,FirstOrDefaultAsync
là sự lựa chọn tốt hơn.
Định tuyến dữ liệu so với chuỗi truy vấn
URL cho trang Details là https://localhost:<port>/Students/Details?id=1
. Giá trị khóa chính của thực thể nằm trong chuỗi truy vấn. Một số nhà phát triển thích chuyển giá trị khóa trong dữ liệu route: https://localhost:<port>/Students/Details/1
. Để biết thêm thông tin, hãy xem Cập nhật code đã sinh.
Cập nhật trang Create
Code scaffold OnPostAsync
cho trang Create dễ bị đăng quá mức (overposting). Ta tiến hành thay thế phương thức OnPostAsync
trong Pages/Students/Create.cshtml.cs
bằng đoạn code sau:
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
Đoạn code trên tạo một đối tượng Student và sau đó sử dụng các trường form đã đăng để cập nhật các property của đối tượng Student. Phương thức TryUpdateModelAsync:
- Sử dụng các giá trị form đã đăng từ property PageContext trong PageModel.
- Chỉ cập nhật các property được liệt kê (
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate
). - Tìm kiếm các trường form có tiền tố "student". Ví dụ như
Student.FirstMidName
. Nó không phân biệt chữ hoa chữ thường. - Sử dụng hệ thống model binding để chuyển đổi các giá trị biểu mẫu từ chuỗi thành các kiểu trong model
Student
. Ví dụ:EnrollmentDate
được chuyển đổi thànhDateTime
.
Chạy ứng dụng và tạo một thực thể sinh viên để kiểm tra trang Create.
Đăng quá mức (Overposting)
Việc sử dụng TryUpdateModel
để cập nhật các trường có giá trị đã đăng là phương pháp bảo mật tốt nhất vì nó ngăn việc đăng quá mức. Ví dụ: giả sử thực thể Student bao gồm một property Secret
mà trang web này không được cập nhật hoặc thêm:
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 Razor Page tạo hoặc cập nhật, tin tặc có thể đặt giá trị Secret
bằng cách đăng quá nhiều. Tin tặc có thể sử dụng một công cụ chẳng hạn như Fiddler hoặc viết một số JavaScript để đăng một giá trị form Secret
. Code ban đầu không giới hạn các trường mà trình model binder sử dụng khi nó tạo một thể hiện Student.
Bất kỳ giá trị nào mà tin tặc chỉ định cho trường form Secret
đều được cập nhật trong cơ sở dữ liệu. Hình ảnh sau đây cho thấy công cụ Fiddler thêm trường Secret
có giá trị "OverPost" vào các giá trị form đã đăng.
Giá trị "OverPost" được thêm thành công vào property Secret
của hàng được chèn. Điều đó xảy ra ngay cả khi nhà thiết kế ứng dụng không bao giờ có ý định đặt property Secret
với trang Create.
Model view
Model view cung cấp một cách khác để ngăn chặn đăng quá mức.
Model ứng dụng thường được gọi là model miền. Model miền thường chứa tất cả các property được yêu cầu bởi thực thể tương ứng trong cơ sở dữ liệu. View model chỉ chứa các property cần thiết cho trang giao diện người dùng, ví dụ: trang Create.
Ngoài model view, một số ứng dụng sử dụng model binding hoặc model input để truyền dữ liệu giữa lớp model trang Razor Pages và trình duyệt.
Xem xét model view StudentVM
sau:
public class StudentVM
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
Đoạn mã sau sử dụng model view StudentVM
để tạo một sinh viên mới:
[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 thiết lập các giá trị của đối tượng này bằng cách đọc các giá trị từ một đối tượng PropertyValues khác. SetValues
sử dụng khớp tên property. Kiểu view model:
- Không cần phải liên quan đến kiểu model.
- Cần phải có các property phù hợp.
Việc sử dụng StudentVM
yêu cầu sử dụng trang Create sử dụng StudentVM
hơn là 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");}
}
Cập nhật trang Edit
Trong Pages/Students/Edit.cshtml.cs
, thay thế các phương thức OnGetAsync
và OnPostAsync
bằng đoạn mã sau.
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 về code tương tự như trang Create với một vài ngoại lệ:
FirstOrDefaultAsync
đã được thay thế bằng FindAsync. Khi bạn không phải bao gồm dữ liệu liên quan,FindAsync
sẽ hiệu quả hơn.OnPostAsync
có một tham sốid
.- Student hiện tại được lấy từ cơ sở dữ liệu, thay vì tạo một student trống.
Chạy ứng dụng và kiểm tra ứng dụng bằng cách tạo và chỉnh sửa student.
Các trạng thái của thực thể
Database context theo dõi xem các thực thể trong bộ nhớ có đồng bộ với các hàng tương ứng của chúng trong cơ sở dữ liệu hay không. Thông tin theo dõi này xác định điều gì sẽ xảy ra khi SaveChangesAsync được gọi. Ví dụ: khi một thực thể mới được chuyển sang phương thức AddAsync, trạng thái của thực thể đó được đặt thành Added. Khi SaveChangesAsync
được gọi, database context sẽ đưa ra một lệnh SQL INSERT
.
Một thực thể có thể ở một trong các trạng thái sau:
Added
: Thực thể chưa tồn tại trong cơ sở dữ liệu. Phương thứcSaveChanges
đưa ra một lệnhINSERT
.Unchanged
: Không cần lưu thay đổi với thực thể này. Một thực thể có trạng thái này khi nó được đọc từ cơ sở dữ liệu.Modified
: Một số hoặc tất cả các giá trị property của thực thể đã được sửa đổi. Phương thứcSaveChanges
đưa ra một lệnhUPDATE
.Deleted
: Thực thể đã được đánh dấu để xóa. Phương thứcSaveChanges
đưa ra một lệnhDELETE
.Detached
: Thực thể không được theo dõi bởi database context.
Trong ứng dụng dành cho máy tính để bàn, các thay đổi trạng thái thường được đặt tự động. Một thực thể được đọc, các thay đổi được thực hiện và trạng thái của thực thể được tự động thay đổi thành Modified
. Lời gọi SaveChanges
sẽ tạo ra một lệnh SQL UPDATE
chỉ cập nhật các property đã thay đổi.
Trong một ứng dụng web, DbContext
đọc một thực thể và hiển thị dữ liệu sẽ được xử lý sau khi một trang được hiển thị. Khi một phương thức OnPostAsync
của trang được gọi, một yêu cầu web mới được thực hiện và với một phiên bản mới của DbContext
. Việc đọc lại thực thể trong ngữ cảnh mới đó sẽ mô phỏng quá trình xử lý trên máy tính để bàn.
Cập nhật trang Delete
Trong phần này, một thông báo lỗi tùy chỉnh được triển khai khi lời gọi tới SaveChanges
không thành công.
Thay thế code trong Pages/Students/Delete.cshtml.cs
bằng đoạn code sau đây:
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 });
}
}
}
}
Trong đoạn code trên:
- Thêm Ghi nhật ký (Logging).
- Thêm tham số tùy chọn
saveChangesError
vào signature phương thứcOnGetAsync
.saveChangesError
cho biết liệu phương thức có được gọi sau khi không thể xóa đối tượng sinh viên hay không.
Thao tác xóa có thể không thành công do sự cố mạng tạm thời. Lỗi mạng tạm thời có nhiều khả năng hơn khi cơ sở dữ liệu ở trên đám mây. Tham số saveChangesError
là false
khi trang Delete OnGetAsync
được gọi từ giao diện người dùng. Khi OnGetAsync
được gọi bởi OnPostAsync
vì thao tác xóa không thành công, thì tham số saveChangesError
là true
.
Phương thức OnPostAsync
truy xuất thực thể đã chọn, sau đó gọi phương thức Remove để đặt trạng thái của thực thể thành Deleted
. Khi SaveChanges
được gọi, một lệnh SQL DELETE
được tạo. Nếu Remove
thất bại thì:
- Ngoại lệ cơ sở dữ liệu sẽ được bắt.
- Phương thức Delete trang
OnGetAsync
được gọi vớisaveChangesError=true
.
Thêm thông báo lỗi vào 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>
Chạy ứng dụng và xóa một student để kiểm tra trang Delete.
Bước tiếp theo
Hướng dẫn trước Hướng dẫn tiếp theo