Udostępnij za pośrednictwem


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

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

Ostrzeżenie

Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz .NET i .NET Core Support Policy (Zasady obsługi platformy .NET Core). Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

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ą, zapoznaj się z wersją tego artykułu platformy .NET 8.

Przez Tom Dykstra, Jeremy Likness i Jon P Smith

Aplikacja internetowa Contoso University pokazuje, jak tworzyć Razor aplikacje internetowe stron przy użyciu 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 kod CRUD (tworzenie, odczytywanie, aktualizowanie i usuwanie) jest przeglądany i dostosowywany.

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 EF Coresamouczku, EF Core kod jest dodawany bezpośrednio do klas modelu strony.

Aktualizowanie strony Szczegóły

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

Rejestracje odczytu

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

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 Metody i ThenInclude powodują załadowanie Student.Enrollments właściwości nawigacji kontekstu i w ramach każdej rejestracji Enrollment.Course właściwości nawigacji. 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 poniższym Pages/Students/Details.cshtml kodem, aby wyświetlić listę rejestracji. 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 przechodzi przez jednostki we Enrollments właściwości nawigacji. Dla każdej rejestracji wyświetla tytuł kursu i ocenę. Tytuł kursu jest pobierany z Course jednostki przechowywanej Course we właściwości nawigacji 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 deweloperzy wolą przekazywać wartość klucza w danych tras: https://localhost:<port>/Students/Details/1. Aby uzyskać więcej informacji, zobacz Aktualizowanie wygenerowanego kodu.

Aktualizowanie strony Tworzenie

Kod szkieletu OnPostAsync strony Tworzenie jest podatny na przesłanie. 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 opublikowanych PageContext wartości formularza z właściwości w obiekcie 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 uwzględnia wielkości 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 element jest konwertowany na DateTime.

Uruchom aplikację i utwórz jednostkę ucznia, aby przetestować stronę Tworzenie.

Zastępowanie

Używanie TryUpdateModel funkcji do aktualizowania pól za pomocą wartości opublikowanych jest najlepszym rozwiązaniem w zakresie zabezpieczeń, ponieważ zapobiega przesłonięciom. 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 Secret pola na stronie tworzenia lub aktualizowania Razor , haker może ustawić Secret wartość przez przesłanie. 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 powiązanie modelu podczas tworzenia wystąpienia ucznia.

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 Wpisu tajnego

Wartość "OverPost" została pomyślnie dodana do Secret właściwości wstawionego wiersza. Dzieje się tak, mimo że projektant aplikacji nigdy nie zamierzał Secret ustawiać właściwości na stronie 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 wyświetlania 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 dopasowania nazwy 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.

Użycie StudentVM polecenia wymaga użycia StudentVM strony Create, a nie 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");}
}

Aktualizowanie strony Edytuj

W Pages/Students/Edit.cshtml.cspliku zastąp OnGetAsync metody 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 element został zastąpiony ciągiem FindAsync. Jeśli nie musisz uwzględniać powiązanych danych, FindAsync jest wydajniejszy.
  • OnPostAsyncid ma parametr .
  • Bieżący student jest pobierany z bazy danych zamiast tworzyć pustego ucznia.

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

Stany jednostek

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 odczytywany 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 odczytujący DbContext jednostkę i wyświetlający dane są usuwane po renderowaniu strony. Po wywołaniu metody strony OnPostAsync zostanie wykonane nowe żądanie internetowe i z nowym wystąpieniem .DbContext Ponowne odczytanie jednostki w tym nowym kontekście symuluje przetwarzanie pulpitu.

Aktualizowanie strony Usuwanie

W tej sekcji jest implementowany niestandardowy komunikat o błędzie, gdy wywołanie zakończy się SaveChanges niepowodzeniem.

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

Powyższy kod ma następujące działanie:

  • 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 wyświetlany false , gdy strona OnGetAsync Usuń jest wywoływana z interfejsu użytkownika. Gdy OnGetAsync jest wywoływana przez OnPostAsync element , ponieważ operacja usuwania nie powiodła się, saveChangesError parametr ma truewartość .

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

  • Przechwycono wyjątek bazy danych.
  • Metoda Delete pages OnGetAsync jest wywoływana za pomocą saveChangesError=truemetody .

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 kod CRUD (tworzenie, odczytywanie, aktualizowanie i usuwanie) jest przeglądany i dostosowywany.

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 EF Coresamouczku, EF Core kod jest dodawany bezpośrednio do klas modelu strony.

Aktualizowanie strony Szczegóły

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

Rejestracje odczytu

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

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 Metody i ThenInclude powodują załadowanie Student.Enrollments właściwości nawigacji kontekstu i w ramach każdej rejestracji Enrollment.Course właściwości nawigacji. 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 poniższym Pages/Students/Details.cshtml kodem, aby wyświetlić listę rejestracji. 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 przechodzi przez jednostki we Enrollments właściwości nawigacji. Dla każdej rejestracji wyświetla tytuł kursu i ocenę. Tytuł kursu jest pobierany z Course jednostki przechowywanej Course we właściwości nawigacji 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 deweloperzy wolą przekazywać wartość klucza w danych tras: https://localhost:<port>/Students/Details/1. Aby uzyskać więcej informacji, zobacz Aktualizowanie wygenerowanego kodu.

Aktualizowanie strony Tworzenie

Kod szkieletu OnPostAsync strony Tworzenie jest podatny na przesłanie. 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 opublikowanych PageContext wartości formularza z właściwości w obiekcie 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 uwzględnia wielkości 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 element jest konwertowany na DateTime.

Uruchom aplikację i utwórz jednostkę ucznia, aby przetestować stronę Tworzenie.

Zastępowanie

Używanie TryUpdateModel funkcji do aktualizowania pól za pomocą wartości opublikowanych jest najlepszym rozwiązaniem w zakresie zabezpieczeń, ponieważ zapobiega przesłonięciom. 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 Secret pola na stronie tworzenia lub aktualizowania Razor , haker może ustawić Secret wartość przez przesłanie. 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 powiązanie modelu podczas tworzenia wystąpienia ucznia.

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 Wpisu tajnego

Wartość "OverPost" została pomyślnie dodana do Secret właściwości wstawionego wiersza. Dzieje się tak, mimo że projektant aplikacji nigdy nie zamierzał Secret ustawiać właściwości na stronie 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 wyświetlania 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 dopasowania nazwy 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.

Użycie StudentVM polecenia wymaga użycia StudentVM strony Create, a nie 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");}
}

Aktualizowanie strony Edytuj

W Pages/Students/Edit.cshtml.cspliku zastąp OnGetAsync metody 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 element został zastąpiony ciągiem FindAsync. Jeśli nie musisz uwzględniać powiązanych danych, FindAsync jest wydajniejszy.
  • OnPostAsyncid ma parametr .
  • Bieżący student jest pobierany z bazy danych zamiast tworzyć pustego ucznia.

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

Stany jednostek

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 odczytywany 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 odczytujący DbContext jednostkę i wyświetlający dane są usuwane po renderowaniu strony. Po wywołaniu metody strony OnPostAsync zostanie wykonane nowe żądanie internetowe i z nowym wystąpieniem .DbContext Ponowne odczytanie jednostki w tym nowym kontekście symuluje przetwarzanie pulpitu.

Aktualizowanie strony Usuwanie

W tej sekcji jest implementowany niestandardowy komunikat o błędzie, gdy wywołanie zakończy się SaveChanges niepowodzeniem.

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

Powyższy kod ma następujące działanie:

  • 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 wyświetlany false , gdy strona OnGetAsync Usuń jest wywoływana z interfejsu użytkownika. Gdy OnGetAsync jest wywoływana przez OnPostAsync element , ponieważ operacja usuwania nie powiodła się, saveChangesError parametr ma truewartość .

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

  • Przechwycono wyjątek bazy danych.
  • Metoda Delete pages OnGetAsync jest wywoływana za pomocą saveChangesError=truemetody .

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 kod CRUD (tworzenie, odczytywanie, aktualizowanie i usuwanie) jest przeglądany i dostosowywany.

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 EF Coresamouczku, EF Core kod jest dodawany bezpośrednio do klas modelu strony.

Aktualizowanie strony Szczegóły

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

Rejestracje odczytu

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 Metody i ThenInclude powodują załadowanie Student.Enrollments właściwości nawigacji kontekstu i w ramach każdej rejestracji Enrollment.Course właściwości nawigacji. 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 poniższym Pages/Students/Details.cshtml kodem, aby wyświetlić listę rejestracji. 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 przechodzi przez jednostki we Enrollments właściwości nawigacji. Dla każdej rejestracji wyświetla tytuł kursu i ocenę. Tytuł kursu jest pobierany z jednostki Course przechowywanej Course we właściwości nawigacji 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 deweloperzy wolą przekazywać wartość klucza w danych tras: https://localhost:<port>/Students/Details/1. Aby uzyskać więcej informacji, zobacz Aktualizowanie wygenerowanego kodu.

Aktualizowanie strony Tworzenie

Kod szkieletu OnPostAsync strony Tworzenie jest podatny na przesłanie. 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 opublikowanych PageContext wartości formularza z właściwości w obiekcie 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 uwzględnia wielkości 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 jednostkę ucznia, aby przetestować stronę Tworzenie.

Zastępowanie

Używanie TryUpdateModel funkcji do aktualizowania pól za pomocą wartości opublikowanych jest najlepszym rozwiązaniem w zakresie zabezpieczeń, ponieważ zapobiega przesłonięciom. 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 Secret pola na stronie tworzenia lub aktualizowania Razor , haker może ustawić Secret wartość przez przesłanie. 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 powiązanie modelu podczas tworzenia wystąpienia ucznia.

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 Wpisu tajnego

Wartość "OverPost" została pomyślnie dodana do Secret właściwości wstawionego wiersza. Dzieje się tak, mimo że projektant aplikacji nigdy nie zamierzał Secret ustawiać właściwości na stronie 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 wyświetlania 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 dopasowania nazwy 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 polecenia wymaga zaktualizowania pliku Create.cshtml do użycia StudentVM , a nie Student.

Aktualizowanie strony Edytuj

W Pages/Students/Edit.cshtml.cspliku zastąp OnGetAsync metody 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 element został zastąpiony ciągiem FindAsync. Jeśli dołączone powiązane dane nie są potrzebne, FindAsync jest bardziej wydajne.
  • OnPostAsyncid ma parametr .
  • Bieżący student jest pobierany z bazy danych zamiast tworzyć pustego ucznia.

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

Stany jednostek

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 odczytywany 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 odczytujący DbContext jednostkę i wyświetlający dane są usuwane po renderowaniu strony. Po wywołaniu metody strony OnPostAsync zostanie wykonane nowe żądanie internetowe i z nowym wystąpieniem .DbContext Ponowne odczytanie jednostki w tym nowym kontekście symuluje przetwarzanie pulpitu.

Aktualizowanie strony Usuwanie

W tej sekcji zaimplementujesz niestandardowy komunikat o błędzie, gdy wywołanie zakończy się SaveChanges niepowodzeniem.

Zamień kod w pliku Pages/Students/Delete.cshtml.cs na następujący kod. Zmiany są wyróżnione (inne niż czyszczenie instrukcji 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 OnGetAsync Usuń jest wywoływana z 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 SaveChanges wywołaniu jest generowane polecenie SQL DELETE. W przypadku Remove niepowodzenia:

  • Przechwycono wyjątek bazy danych.
  • Metoda strony Delete jest wywoływana OnGetAsync za pomocą saveChangesError=truemetody .

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