Udostępnij za pośrednictwem


Część 2, Razor strony z wykorzystaniem EF Core w ASP.NET Core — CRUD

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zobacz artykuł w wersji .NET 9.

Ostrzeżenie

Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz zasady pomocy technicznej platformy .NET i platformy .NET Core. Aby zapoznać się z bieżącą wersją, zobacz artykuł w wersji .NET 9.

Ważne

Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Aby zapoznać się z bieżącą wersją, zobacz artykuł w wersji .NET 9.

Przez Tom Dykstra, Jeremy Likness i Jon P Smith

Aplikacja internetowa Contoso University pokazuje, jak tworzyć Razor aplikacje webowe za pomocą programu EF Core Visual Studio. Aby uzyskać informacje na temat serii samouczków, zobacz pierwszy samouczek.

Jeśli napotkasz problemy, których nie możesz rozwiązać, pobierz ukończoną aplikację i porównaj ten kod z utworzonymi elementami, wykonując czynności opisane w samouczku.

W tym samouczku przeglądany i dostosowywany jest kod CRUD (tworzenie, odczytywanie, aktualizacja, usuwanie) przygotowany wstępnie.

Brak repozytorium

Niektórzy deweloperzy używają warstwy usługi lub wzorca repozytorium, aby utworzyć warstwę abstrakcji między interfejsem użytkownika (Razor stron) i warstwą dostępu do danych. Ten samouczek tego nie robi. Aby zminimalizować złożoność i skoncentrować się na samouczku EF Core, kod EF Core dodaje się bezpośrednio do klas strony modelu.

Aktualizowanie strony Szczegóły

Kod szkieletowy stron Uczniów nie zawiera danych rejestracji. W tej sekcji rejestracje są dodawane do strony Details.

Przeglądanie zapisów

Aby wyświetlić dane rejestracji ucznia na stronie, należy odczytać dane rejestracji. Kod szablonowy w Pages/Students/Details.cshtml.cs odczytuje tylko dane Student, bez danych 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();
}

Zastąp metodę OnGetAsync poniższym kodem, aby odczytać dane rejestracji dla wybranego ucznia. Zmiany są wyróżnione.

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

Include i ThenInclude powodują, że kontekst ładuje właściwość nawigacji Student.Enrollments oraz w ramach każdej rejestracji właściwość nawigacji Enrollment.Course. Te metody zostały szczegółowo zbadane w samouczku Odczyt powiązanych danych .

Metoda AsNoTracking poprawia wydajność w scenariuszach, w których zwracane jednostki nie są aktualizowane w bieżącym kontekście. AsNoTracking zostanie omówiony w dalszej części tego samouczka.

Wyświetlanie rejestracji

Zastąp kod w Pages/Students/Details.cshtml poniższym kodem, aby wyświetlić listę zapisów. Zmiany są wyróżnione.

@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>

Poprzedni kod wchodzi w pętlę przez encje w właściwości nawigacyjnej Enrollments. Dla każdej rejestracji wyświetla tytuł kursu i ocenę. Tytuł kursu jest pobierany z encji Course, która jest przechowywana we właściwości nawigacyjnej Course encji Enrollments.

Uruchom aplikację, wybierz kartę Uczniowie , a następnie kliknij link Szczegóły dla ucznia. Zostanie wyświetlona lista kursów i ocen dla wybranego ucznia.

Sposoby odczytywania jednej jednostki

Wygenerowany kod używa metody FirstOrDefaultAsync do odczytywania jednej jednostki. Ta metoda zwraca wartość null, jeśli nic nie zostanie znalezione; W przeciwnym razie zwraca pierwszy wiersz znaleziony, który spełnia kryteria filtru zapytania. FirstOrDefaultAsync ogólnie jest lepszym wyborem niż następujące alternatywy:

  • SingleOrDefaultAsync — zgłasza wyjątek, jeśli istnieje więcej niż jedna jednostka, która spełnia filtr zapytania. Aby określić, czy zapytanie może zwrócić więcej niż jeden wiersz, SingleOrDefaultAsync spróbuje pobrać wiele wierszy. Ta dodatkowa praca jest niepotrzebna, jeśli zapytanie może zwrócić tylko jedną jednostkę, tak jak podczas wyszukiwania w unikatowym kluczu.
  • FindAsync — znajduje jednostkę z kluczem podstawowym (PK). Jeśli jednostka z kluczem PK jest śledzona przez kontekst, jest zwracana bez żądania do bazy danych. Ta metoda jest zoptymalizowana pod kątem wyszukiwania pojedynczej jednostki, ale nie można wywołać Include metody za pomocą polecenia FindAsync. Dlatego jeśli potrzebne są powiązane dane, FirstOrDefaultAsync jest lepszym wyborem.

Kierowanie danych a ciąg zapytania

Adres URL strony Szczegóły to https://localhost:<port>/Students/Details?id=1. Wartość klucza podstawowego jednostki znajduje się w ciągu zapytania. Niektórzy programiści wolą przekazywać wartość klucza w danych tras: https://localhost:<port>/Students/Details/1. Aby uzyskać więcej informacji, zobacz Aktualizowanie wygenerowanego kodu.

Zaktualizuj stronę tworzenia

Kod wygenerowany OnPostAsync dla strony Tworzenie jest podatny na nadmierne przesyłanie danych. Zastąp metodę OnPostAsync w pliku Pages/Students/Create.cshtml.cs poniższym kodem.

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

Powyższy kod tworzy obiekt Student, a następnie używa pól formularza opublikowanego do aktualizowania właściwości obiektu Student. Metoda TryUpdateModelAsync:

  • Używa przesłanych wartości formularza z właściwości PageContext w PageModel.
  • Aktualizuje tylko właściwości wymienione (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Szuka pól formularza z prefiksem "student". Na przykład Student.FirstMidName. Nie jest wrażliwe na wielkość liter.
  • Używa systemu powiązania modelu do konwertowania wartości formularzy z ciągów na typy w Student modelu. Na przykład EnrollmentDate jest konwertowane na DateTime.

Uruchom aplikację i utwórz obiekt ucznia, aby przetestować stronę tworzenia.

Nadmierne publikowanie

Używanie TryUpdateModel do aktualizowania pól wartościami zamieszczonymi jest najlepszym rozwiązaniem w zakresie bezpieczeństwa, ponieważ zapobiega nadpisywaniu danych. Załóżmy na przykład, że jednostka Student zawiera Secret właściwość, którą ta strona sieci Web nie powinna aktualizować ani dodawać:

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

Nawet jeśli aplikacja nie ma pola Secret na stronie tworzenia lub aktualizowania Razor, haker może ustawić wartość Secret przez nadpisanie. Haker może użyć narzędzia takiego jak Fiddler lub napisać kod JavaScript, aby opublikować Secret wartość formularza. Oryginalny kod nie ogranicza pól używanych przez model binder podczas tworzenia wystąpienia klasy Student.

Dowolna wartość określona przez hakera Secret dla pola formularza jest aktualizowana w bazie danych. Na poniższej ilustracji przedstawiono narzędzie Fiddler dodające Secret pole z wartością "OverPost" do opublikowanych wartości formularza.

Narzędzie Fiddler dodające pole tajne

Wartość "OverPost" została pomyślnie dodana do właściwości Secret wstawionego wiersza. Dzieje się tak, mimo że projektant aplikacji nigdy nie zamierzał Secret ustawiać właściwości za pomocą strony Tworzenie.

Wyświetlanie modelu

Wyświetlanie modeli zapewnia alternatywny sposób zapobiegania przesłonięć.

Model aplikacji jest często nazywany modelem domeny. Model domeny zwykle zawiera wszystkie właściwości wymagane przez odpowiednią jednostkę w bazie danych. Model widoku zawiera tylko właściwości wymagane dla strony interfejsu użytkownika, na przykład strony Tworzenie.

Oprócz modelu wyświetlania niektóre aplikacje używają modelu powiązania lub modelu wejściowego do przekazywania danych między Razor klasą modelu stron a przeglądarką.

Rozważmy następujący StudentVM model widoku:

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

Poniższy kod używa modelu widoku StudentVM do utworzenia nowego ucznia.

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

Metoda SetValues ustawia wartości tego obiektu, odczytując wartości z innego PropertyValues obiektu. SetValues używa dopasowywania nazw właściwości. Typ modelu widoku:

  • Nie musi być powiązany z typem modelu.
  • Musi mieć właściwości, które są zgodne.

Wymagane jest użycie StudentVM na stronie Create zamiast StudentVM:

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

Zaktualizuj stronę edycji

W Pages/Students/Edit.cshtml.cs zastąp metody OnGetAsync i OnPostAsync następującym kodem.

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

Zmiany kodu są podobne do strony Tworzenie z kilkoma wyjątkami:

  • FirstOrDefaultAsync został zastąpiony przez FindAsync. Jeśli nie musisz uwzględniać powiązanych danych, FindAsync jest wydajniejszy.
  • OnPostAsync ma parametr id.
  • Bieżący student jest pobierany z bazy danych zamiast tworzenia pustego studenta.

Uruchom aplikację i przetestuj ją, tworząc i edytując ucznia.

Stany podmiotów

Kontekst bazy danych śledzi, czy jednostki w pamięci są zsynchronizowane z odpowiednimi wierszami w bazie danych. Te informacje śledzenia określają, co się stanie po wywołaniu polecenia SaveChangesAsync . Na przykład po przekazaniu nowej jednostki do AddAsync metody stan tej jednostki ma wartość Added. Po SaveChangesAsync wywołaniu kontekst bazy danych wystawia polecenie SQL INSERT .

Jednostka może znajdować się w jednym z następujących stanów:

  • Added: jednostka nie istnieje jeszcze w bazie danych. Metoda SaveChanges wydaje oświadczenie INSERT.

  • Unchanged: nie trzeba zapisywać żadnych zmian w tej jednostce. Jednostka ma ten stan, gdy jest odczytywana z bazy danych.

  • Modified: Niektóre lub wszystkie wartości właściwości jednostki zostały zmodyfikowane. Metoda SaveChanges wydaje oświadczenie UPDATE.

  • Deleted: Jednostka została oznaczona do usunięcia. Metoda SaveChanges wystawia oświadczenie DELETE .

  • Detached: jednostka nie jest śledzona przez kontekst bazy danych.

W aplikacji klasycznej zmiany stanu są zwykle ustawiane automatycznie. Jednostka jest odczytywana, wprowadzana jest zmiana, a stan jednostki jest automatycznie zmieniany na Modified. Wywołanie SaveChanges generuje instrukcję SQL UPDATE , która aktualizuje tylko zmienione właściwości.

W aplikacji internetowej element, który odczytuje jednostkę DbContext i wyświetla dane, jest usuwany po renderowaniu strony. Po wywołaniu metody strony OnPostAsync, zostanie wykonane nowe żądanie internetowe z nowym wystąpieniem DbContext. Ponowne odczytanie danych w tym nowym kontekście symuluje przetwarzanie przez komputer stacjonarny.

Zaktualizuj stronę usuwania

W tej sekcji implementowany jest niestandardowy komunikat o błędzie, gdy wywołanie SaveChanges nie powiedzie się.

Zastąp kod w pliku Pages/Students/Delete.cshtml.cs następującym kodem:

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

Poprzedni kod:

  • Dodaje rejestrowanie.
  • Dodaje opcjonalny parametr saveChangesError do OnGetAsync podpisu metody. saveChangesError wskazuje, czy metoda została wywołana po niepowodzeniu usunięcia obiektu ucznia.

Operacja usuwania może zakończyć się niepowodzeniem z powodu przejściowych problemów z siecią. Przejściowe błędy sieci są bardziej prawdopodobne, gdy baza danych znajduje się w chmurze. Parametr saveChangesError jest false , gdy strona Usuń OnGetAsync jest wywoływana z interfejsu użytkownika. Gdy OnGetAsync jest wywoływane przez OnPostAsync, ponieważ operacja usuwania nie powiodła się, wartość parametru saveChangesError jest true.

Metoda OnPostAsync pobiera wybraną jednostkę, a następnie wywołuje metodę Remove , aby ustawić stan jednostki na Deleted. Po wywołaniu SaveChanges, generowane jest polecenie SQL DELETE. W przypadku Remove niepowodzenia:

  • Przechwycono wyjątek bazy danych.
  • Metoda OnGetAsync Delete pages jest wywoływana z saveChangesError=true.

Dodaj komunikat o błędzie do :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>

Uruchom aplikację i usuń ucznia, aby przetestować stronę Usuń.

Następne kroki

W tym samouczku przeglądany i dostosowywany jest kod CRUD (tworzenie, odczytywanie, aktualizacja, usuwanie) przygotowany wstępnie.

Brak repozytorium

Niektórzy deweloperzy używają warstwy usługi lub wzorca repozytorium, aby utworzyć warstwę abstrakcji między interfejsem użytkownika (Razor stron) i warstwą dostępu do danych. Ten samouczek tego nie robi. Aby zminimalizować złożoność i skoncentrować się na samouczku EF Core, kod EF Core dodaje się bezpośrednio do klas strony modelu.

Aktualizowanie strony Szczegóły

Kod szkieletowy stron Uczniów nie zawiera danych rejestracji. W tej sekcji rejestracje są dodawane do strony Details.

Przeglądanie zapisów

Aby wyświetlić dane rejestracji ucznia na stronie, należy odczytać dane rejestracji. Kod szablonowy w Pages/Students/Details.cshtml.cs odczytuje tylko dane Student, bez danych 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();
}

Zastąp metodę OnGetAsync poniższym kodem, aby odczytać dane rejestracji dla wybranego ucznia. Zmiany są wyróżnione.

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

Include i ThenInclude powodują, że kontekst ładuje właściwość nawigacji Student.Enrollments oraz w ramach każdej rejestracji właściwość nawigacji Enrollment.Course. Te metody zostały szczegółowo zbadane w samouczku Odczyt powiązanych danych .

Metoda AsNoTracking poprawia wydajność w scenariuszach, w których zwracane jednostki nie są aktualizowane w bieżącym kontekście. AsNoTracking zostanie omówiony w dalszej części tego samouczka.

Wyświetlanie rejestracji

Zastąp kod w Pages/Students/Details.cshtml poniższym kodem, aby wyświetlić listę zapisów. Zmiany są wyróżnione.

@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>

Poprzedni kod wchodzi w pętlę przez encje w właściwości nawigacyjnej Enrollments. Dla każdej rejestracji wyświetla tytuł kursu i ocenę. Tytuł kursu jest pobierany z encji Course, która jest przechowywana we właściwości nawigacyjnej Course encji Enrollments.

Uruchom aplikację, wybierz kartę Uczniowie , a następnie kliknij link Szczegóły dla ucznia. Zostanie wyświetlona lista kursów i ocen dla wybranego ucznia.

Sposoby odczytywania jednej jednostki

Wygenerowany kod używa metody FirstOrDefaultAsync do odczytywania jednej jednostki. Ta metoda zwraca wartość null, jeśli nic nie zostanie znalezione; W przeciwnym razie zwraca pierwszy wiersz znaleziony, który spełnia kryteria filtru zapytania. FirstOrDefaultAsync ogólnie jest lepszym wyborem niż następujące alternatywy:

  • SingleOrDefaultAsync — zgłasza wyjątek, jeśli istnieje więcej niż jedna jednostka, która spełnia filtr zapytania. Aby określić, czy zapytanie może zwrócić więcej niż jeden wiersz, SingleOrDefaultAsync spróbuje pobrać wiele wierszy. Ta dodatkowa praca jest niepotrzebna, jeśli zapytanie może zwrócić tylko jedną jednostkę, tak jak podczas wyszukiwania w unikatowym kluczu.
  • FindAsync — znajduje jednostkę z kluczem podstawowym (PK). Jeśli jednostka z kluczem PK jest śledzona przez kontekst, jest zwracana bez żądania do bazy danych. Ta metoda jest zoptymalizowana pod kątem wyszukiwania pojedynczej jednostki, ale nie można wywołać Include metody za pomocą polecenia FindAsync. Dlatego jeśli potrzebne są powiązane dane, FirstOrDefaultAsync jest lepszym wyborem.

Kierowanie danych a ciąg zapytania

Adres URL strony Szczegóły to https://localhost:<port>/Students/Details?id=1. Wartość klucza podstawowego jednostki znajduje się w ciągu zapytania. Niektórzy programiści wolą przekazywać wartość klucza w danych tras: https://localhost:<port>/Students/Details/1. Aby uzyskać więcej informacji, zobacz Aktualizowanie wygenerowanego kodu.

Zaktualizuj stronę tworzenia

Kod wygenerowany OnPostAsync dla strony Tworzenie jest podatny na nadmierne przesyłanie danych. Zastąp metodę OnPostAsync w pliku Pages/Students/Create.cshtml.cs poniższym kodem.

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

Powyższy kod tworzy obiekt Student, a następnie używa pól formularza opublikowanego do aktualizowania właściwości obiektu Student. Metoda TryUpdateModelAsync:

  • Używa przesłanych wartości formularza z właściwości PageContext w PageModel.
  • Aktualizuje tylko właściwości wymienione (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Szuka pól formularza z prefiksem "student". Na przykład Student.FirstMidName. Nie jest wrażliwe na wielkość liter.
  • Używa systemu powiązania modelu do konwertowania wartości formularzy z ciągów na typy w Student modelu. Na przykład EnrollmentDate jest konwertowane na DateTime.

Uruchom aplikację i utwórz obiekt ucznia, aby przetestować stronę tworzenia.

Nadmierne publikowanie

Używanie TryUpdateModel do aktualizowania pól wartościami zamieszczonymi jest najlepszym rozwiązaniem w zakresie bezpieczeństwa, ponieważ zapobiega nadpisywaniu danych. Załóżmy na przykład, że jednostka Student zawiera Secret właściwość, którą ta strona sieci Web nie powinna aktualizować ani dodawać:

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

Nawet jeśli aplikacja nie ma pola Secret na stronie tworzenia lub aktualizowania Razor, haker może ustawić wartość Secret przez nadpisanie. Haker może użyć narzędzia takiego jak Fiddler lub napisać kod JavaScript, aby opublikować Secret wartość formularza. Oryginalny kod nie ogranicza pól używanych przez model binder podczas tworzenia wystąpienia klasy Student.

Dowolna wartość określona przez hakera Secret dla pola formularza jest aktualizowana w bazie danych. Na poniższej ilustracji przedstawiono narzędzie Fiddler dodające Secret pole z wartością "OverPost" do opublikowanych wartości formularza.

Narzędzie Fiddler dodające pole tajne

Wartość "OverPost" została pomyślnie dodana do właściwości Secret wstawionego wiersza. Dzieje się tak, mimo że projektant aplikacji nigdy nie zamierzał Secret ustawiać właściwości za pomocą strony Tworzenie.

Wyświetlanie modelu

Wyświetlanie modeli zapewnia alternatywny sposób zapobiegania przesłonięć.

Model aplikacji jest często nazywany modelem domeny. Model domeny zwykle zawiera wszystkie właściwości wymagane przez odpowiednią jednostkę w bazie danych. Model widoku zawiera tylko właściwości wymagane dla strony interfejsu użytkownika, na przykład strony Tworzenie.

Oprócz modelu wyświetlania niektóre aplikacje używają modelu powiązania lub modelu wejściowego do przekazywania danych między Razor klasą modelu stron a przeglądarką.

Rozważmy następujący StudentVM model widoku:

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

Poniższy kod używa modelu widoku StudentVM do utworzenia nowego ucznia.

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

Metoda SetValues ustawia wartości tego obiektu, odczytując wartości z innego PropertyValues obiektu. SetValues używa dopasowywania nazw właściwości. Typ modelu widoku:

  • Nie musi być powiązany z typem modelu.
  • Musi mieć właściwości, które są zgodne.

Wymagane jest użycie StudentVM na stronie Create zamiast StudentVM:

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

Zaktualizuj stronę edycji

W Pages/Students/Edit.cshtml.cs zastąp metody OnGetAsync i OnPostAsync następującym kodem.

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

Zmiany kodu są podobne do strony Tworzenie z kilkoma wyjątkami:

  • FirstOrDefaultAsync został zastąpiony przez FindAsync. Jeśli nie musisz uwzględniać powiązanych danych, FindAsync jest wydajniejszy.
  • OnPostAsync ma parametr id.
  • Bieżący student jest pobierany z bazy danych zamiast tworzenia pustego studenta.

Uruchom aplikację i przetestuj ją, tworząc i edytując ucznia.

Stany podmiotów

Kontekst bazy danych śledzi, czy jednostki w pamięci są zsynchronizowane z odpowiednimi wierszami w bazie danych. Te informacje śledzenia określają, co się stanie po wywołaniu polecenia SaveChangesAsync . Na przykład po przekazaniu nowej jednostki do AddAsync metody stan tej jednostki ma wartość Added. Po SaveChangesAsync wywołaniu kontekst bazy danych wystawia polecenie SQL INSERT .

Jednostka może znajdować się w jednym z następujących stanów:

  • Added: jednostka nie istnieje jeszcze w bazie danych. Metoda SaveChanges wydaje oświadczenie INSERT.

  • Unchanged: nie trzeba zapisywać żadnych zmian w tej jednostce. Jednostka ma ten stan, gdy jest odczytywana z bazy danych.

  • Modified: Niektóre lub wszystkie wartości właściwości jednostki zostały zmodyfikowane. Metoda SaveChanges wydaje oświadczenie UPDATE.

  • Deleted: Jednostka została oznaczona do usunięcia. Metoda SaveChanges wystawia oświadczenie DELETE .

  • Detached: jednostka nie jest śledzona przez kontekst bazy danych.

W aplikacji klasycznej zmiany stanu są zwykle ustawiane automatycznie. Jednostka jest odczytywana, wprowadzana jest zmiana, a stan jednostki jest automatycznie zmieniany na Modified. Wywołanie SaveChanges generuje instrukcję SQL UPDATE , która aktualizuje tylko zmienione właściwości.

W aplikacji internetowej element, który odczytuje jednostkę DbContext i wyświetla dane, jest usuwany po renderowaniu strony. Po wywołaniu metody strony OnPostAsync, zostanie wykonane nowe żądanie internetowe z nowym wystąpieniem DbContext. Ponowne odczytanie danych w tym nowym kontekście symuluje przetwarzanie przez komputer stacjonarny.

Zaktualizuj stronę usuwania

W tej sekcji implementowany jest niestandardowy komunikat o błędzie, gdy wywołanie SaveChanges nie powiedzie się.

Zastąp kod w pliku Pages/Students/Delete.cshtml.cs następującym kodem:

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

Poprzedni kod:

  • Dodaje rejestrowanie.
  • Dodaje opcjonalny parametr saveChangesError do OnGetAsync podpisu metody. saveChangesError wskazuje, czy metoda została wywołana po niepowodzeniu usunięcia obiektu ucznia.

Operacja usuwania może zakończyć się niepowodzeniem z powodu przejściowych problemów z siecią. Przejściowe błędy sieci są bardziej prawdopodobne, gdy baza danych znajduje się w chmurze. Parametr saveChangesError jest false , gdy strona Usuń OnGetAsync jest wywoływana z interfejsu użytkownika. Gdy OnGetAsync jest wywoływane przez OnPostAsync, ponieważ operacja usuwania nie powiodła się, wartość parametru saveChangesError jest true.

Metoda OnPostAsync pobiera wybraną jednostkę, a następnie wywołuje metodę Remove , aby ustawić stan jednostki na Deleted. Po wywołaniu SaveChanges, generowane jest polecenie SQL DELETE. W przypadku Remove niepowodzenia:

  • Przechwycono wyjątek bazy danych.
  • Metoda OnGetAsync Delete pages jest wywoływana z saveChangesError=true.

Dodaj komunikat o błędzie do :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>

Uruchom aplikację i usuń ucznia, aby przetestować stronę Usuń.

Następne kroki

W tym samouczku przeglądany i dostosowywany jest kod CRUD (tworzenie, odczytywanie, aktualizacja, usuwanie) przygotowany wstępnie.

Brak repozytorium

Niektórzy deweloperzy używają warstwy usługi lub wzorca repozytorium, aby utworzyć warstwę abstrakcji między interfejsem użytkownika (Razor stron) i warstwą dostępu do danych. Ten samouczek tego nie robi. Aby zminimalizować złożoność i skoncentrować się na samouczku EF Core, kod EF Core dodaje się bezpośrednio do klas strony modelu.

Aktualizowanie strony Szczegóły

Kod szkieletowy stron Uczniów nie zawiera danych rejestracji. W tej sekcji rejestracje są dodawane do strony Szczegóły.

Przeglądanie zapisów

Aby wyświetlić dane rejestracji ucznia na stronie, należy odczytać dane rejestracji. Kod szkieletowy w pliku Pages/Students/Details.cshtml.cs odczytuje tylko dane ucznia bez danych rejestracji:

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

Zastąp metodę OnGetAsync poniższym kodem, aby odczytać dane rejestracji dla wybranego ucznia. Zmiany są wyróżnione.

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

Include i ThenInclude powodują, że kontekst ładuje właściwość nawigacji Student.Enrollments oraz w ramach każdej rejestracji właściwość nawigacji Enrollment.Course. Te metody zostały szczegółowo zbadane w samouczku Dotyczącym danych związanych z czytaniem.

Metoda AsNoTracking poprawia wydajność w scenariuszach, w których zwracane jednostki nie są aktualizowane w bieżącym kontekście. AsNoTracking zostanie omówiony w dalszej części tego samouczka.

Wyświetlanie rejestracji

Zastąp kod w Pages/Students/Details.cshtml poniższym kodem, aby wyświetlić listę zapisów. Zmiany są wyróżnione.

@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>

Poprzedni kod wchodzi w pętlę przez encje w właściwości nawigacyjnej Enrollments. Dla każdej rejestracji wyświetla tytuł kursu i ocenę. Tytuł kursu jest pobierany z jednostki Kursu, która jest przechowywana we właściwości nawigacyjnej jednostki Enrollments.

Uruchom aplikację, wybierz kartę Uczniowie , a następnie kliknij link Szczegóły dla ucznia. Zostanie wyświetlona lista kursów i ocen dla wybranego ucznia.

Sposoby odczytywania jednej jednostki

Wygenerowany kod używa metody FirstOrDefaultAsync do odczytywania jednej jednostki. Ta metoda zwraca wartość null, jeśli nic nie zostanie znalezione; W przeciwnym razie zwraca pierwszy wiersz znaleziony, który spełnia kryteria filtru zapytania. FirstOrDefaultAsync ogólnie jest lepszym wyborem niż następujące alternatywy:

  • SingleOrDefaultAsync — zgłasza wyjątek, jeśli istnieje więcej niż jedna jednostka, która spełnia filtr zapytania. Aby określić, czy zapytanie może zwrócić więcej niż jeden wiersz, SingleOrDefaultAsync spróbuje pobrać wiele wierszy. Ta dodatkowa praca jest niepotrzebna, jeśli zapytanie może zwrócić tylko jedną jednostkę, tak jak podczas wyszukiwania w unikatowym kluczu.
  • FindAsync — znajduje jednostkę z kluczem podstawowym (PK). Jeśli jednostka z kluczem PK jest śledzona przez kontekst, jest zwracana bez żądania do bazy danych. Ta metoda jest zoptymalizowana pod kątem wyszukiwania pojedynczej jednostki, ale nie można wywołać Include metody za pomocą polecenia FindAsync. Dlatego jeśli potrzebne są powiązane dane, FirstOrDefaultAsync jest lepszym wyborem.

Kierowanie danych a ciąg zapytania

Adres URL strony Szczegóły to https://localhost:<port>/Students/Details?id=1. Wartość klucza podstawowego jednostki znajduje się w ciągu zapytania. Niektórzy programiści wolą przekazywać wartość klucza w danych tras: https://localhost:<port>/Students/Details/1. Aby uzyskać więcej informacji, zobacz Aktualizowanie wygenerowanego kodu.

Zaktualizuj stronę tworzenia

Kod wygenerowany OnPostAsync dla strony Tworzenie jest podatny na nadmierne przesyłanie danych. Zastąp metodę OnPostAsync w pliku Pages/Students/Create.cshtml.cs poniższym kodem.

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

Powyższy kod tworzy obiekt Student, a następnie używa pól formularza opublikowanego do aktualizowania właściwości obiektu Student. Metoda TryUpdateModelAsync:

  • Używa przesłanych wartości formularza z właściwości PageContext w PageModel.
  • Aktualizuje tylko właściwości wymienione (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Szuka pól formularza z prefiksem "student". Na przykład Student.FirstMidName. Nie jest wrażliwe na wielkość liter.
  • Używa systemu powiązania modelu do konwertowania wartości formularzy z ciągów na typy w Student modelu. Na przykład EnrollmentDate należy przekonwertować na datetime.

Uruchom aplikację i utwórz obiekt ucznia, aby przetestować stronę tworzenia.

Nadmierne publikowanie

Używanie TryUpdateModel do aktualizowania pól wartościami zamieszczonymi jest najlepszym rozwiązaniem w zakresie bezpieczeństwa, ponieważ zapobiega nadpisywaniu danych. Załóżmy na przykład, że jednostka Student zawiera Secret właściwość, którą ta strona sieci Web nie powinna aktualizować ani dodawać:

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

Nawet jeśli aplikacja nie ma pola Secret na stronie tworzenia lub aktualizowania Razor, haker może ustawić wartość Secret przez nadpisanie. Haker może użyć narzędzia takiego jak Fiddler lub napisać kod JavaScript, aby opublikować Secret wartość formularza. Oryginalny kod nie ogranicza pól używanych przez model binder podczas tworzenia wystąpienia klasy Student.

Dowolna wartość określona przez hakera Secret dla pola formularza jest aktualizowana w bazie danych. Na poniższym obrazie pokazano, jak narzędzie Fiddler dodaje pole Secret (z wartością "OverPost") do wysłanych wartości formularza.

Narzędzie Fiddler dodające pole tajne

Wartość "OverPost" została pomyślnie dodana do właściwości Secret wstawionego wiersza. Dzieje się tak, mimo że projektant aplikacji nigdy nie zamierzał Secret ustawiać właściwości za pomocą strony Tworzenie.

Wyświetlanie modelu

Wyświetlanie modeli zapewnia alternatywny sposób zapobiegania przesłonięć.

Model aplikacji jest często nazywany modelem domeny. Model domeny zwykle zawiera wszystkie właściwości wymagane przez odpowiednią jednostkę w bazie danych. Model widoku zawiera tylko właściwości wymagane dla interfejsu użytkownika, dla którego jest używany (na przykład strona Tworzenie).

Oprócz modelu wyświetlania niektóre aplikacje używają modelu powiązania lub modelu wejściowego do przekazywania danych między Razor klasą modelu stron a przeglądarką.

Rozważmy następujący Student model widoku:

using System;

namespace ContosoUniversity.Models
{
    public class StudentVM
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }
    }
}

Poniższy kod używa modelu widoku StudentVM do utworzenia nowego ucznia.

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

Metoda SetValues ustawia wartości tego obiektu, odczytując wartości z innego PropertyValues obiektu. SetValues używa dopasowywania nazw właściwości. Typ modelu widoku nie musi być powiązany z typem modelu, ale musi mieć zgodne właściwości.

Użycie StudentVM wymaga zaktualizowania pliku Create.cshtml, aby korzystać z StudentVM zamiast Student.

Zaktualizuj stronę edycji

W Pages/Students/Edit.cshtml.cs zastąp metody OnGetAsync i OnPostAsync następującym kodem.

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

Zmiany kodu są podobne do strony Tworzenie z kilkoma wyjątkami:

  • FirstOrDefaultAsync został zastąpiony przez FindAsync. Jeśli dołączone powiązane dane nie są potrzebne, FindAsync jest bardziej wydajne.
  • OnPostAsync ma parametr id.
  • Bieżący student jest pobierany z bazy danych zamiast tworzenia pustego studenta.

Uruchom aplikację i przetestuj ją, tworząc i edytując ucznia.

Stany podmiotów

Kontekst bazy danych śledzi, czy jednostki w pamięci są zsynchronizowane z odpowiednimi wierszami w bazie danych. Te informacje śledzenia określają, co się stanie po wywołaniu polecenia SaveChangesAsync . Na przykład po przekazaniu nowej jednostki do AddAsync metody stan tej jednostki ma wartość Added. Po SaveChangesAsync wywołaniu kontekst bazy danych wystawia polecenie SQL INSERT.

Jednostka może znajdować się w jednym z następujących stanów:

  • Added: jednostka nie istnieje jeszcze w bazie danych. Metoda SaveChanges wystawia instrukcję INSERT.

  • Unchanged: nie trzeba zapisywać żadnych zmian w tej jednostce. Jednostka ma ten stan, gdy jest odczytywana z bazy danych.

  • Modified: Niektóre lub wszystkie wartości właściwości jednostki zostały zmodyfikowane. Metoda SaveChanges wystawia instrukcję UPDATE.

  • Deleted: Jednostka została oznaczona do usunięcia. Metoda SaveChanges wystawia instrukcję DELETE.

  • Detached: jednostka nie jest śledzona przez kontekst bazy danych.

W aplikacji klasycznej zmiany stanu są zwykle ustawiane automatycznie. Jednostka jest odczytywana, wprowadzana jest zmiana, a stan jednostki jest automatycznie zmieniany na Modified. Wywołanie SaveChanges generuje instrukcję SQL UPDATE, która aktualizuje tylko zmienione właściwości.

W aplikacji internetowej element, który odczytuje jednostkę DbContext i wyświetla dane, jest usuwany po renderowaniu strony. Po wywołaniu metody strony OnPostAsync, zostanie wykonane nowe żądanie internetowe z nowym wystąpieniem DbContext. Ponowne odczytanie danych w tym nowym kontekście symuluje przetwarzanie przez komputer stacjonarny.

Zaktualizuj stronę usuwania

Niestandardowy komunikat o błędzie zaimplementujesz w tej sekcji, gdy wywołanie do SaveChanges nie powiedzie się.

Zamień kod w pliku Pages/Students/Delete.cshtml.cs na następujący kod. Zmiany są wyróżnione (inne niż czyszczenie oświadczeń using).

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [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 = "Delete failed. Try again";
            }

            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 */)
            {
                //Log the error (uncomment ex variable name and write a log.)
                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

Powyższy kod dodaje opcjonalny parametr saveChangesError do OnGetAsync podpisu metody. saveChangesError wskazuje, czy metoda została wywołana po niepowodzeniu usunięcia obiektu ucznia. Operacja usuwania może zakończyć się niepowodzeniem z powodu przejściowych problemów z siecią. Przejściowe błędy sieci są bardziej prawdopodobne, gdy baza danych znajduje się w chmurze. Parametr saveChangesError ma wartość false, gdy strona Usuń OnGetAsync jest wywoływana z poziomu interfejsu użytkownika. Gdy OnGetAsync parametr jest wywoływany przez OnPostAsync (ponieważ operacja usuwania nie powiodła się), saveChangesError parametr ma wartość true.

Metoda OnPostAsync pobiera wybraną jednostkę, a następnie wywołuje metodę Remove , aby ustawić stan jednostki na Deleted. Po wywołaniu SaveChanges generowane jest polecenie SQL DELETE. W przypadku Remove niepowodzenia:

  • Przechwycono wyjątek bazy danych.
  • Metoda Delete strony jest wywoływana OnGetAsync przy użyciu saveChangesError=true.

Dodaj komunikat o błędzie do strony Usuń Razor (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>

Uruchom aplikację i usuń ucznia, aby przetestować stronę Usuń.

Następne kroki