ASP.NET Core: Razor Pages với Entity Framework Core trong ASP.NET Core - CRUD - Hướng dẫn 2/8


Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên

Ứ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ằng FindAsync. 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ành DateTime.

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.

Fiddler thêm trường Secret

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ức SaveChanges đưa ra một lệnh INSERT.
  • 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ức SaveChanges đưa ra một lệnh UPDATE.
  • Deleted: Thực thể đã được đánh dấu để xóa. Phương thức SaveChanges đưa ra một lệnh DELETE.
  • 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ức OnGetAsyncsaveChangesError 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ới saveChangesError=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

Nguồn: learn.microsoft.com
» Tiếp: Razor Pages với Entity Framework Core trong ASP.NET Core - Sort, Filter, Paging - Hướng dẫn 3/8
« Trước: Razor Pages với Entity Framework Core trong ASP.NET Core - Hướng dẫn 1/8
Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên
Copied !!!