Lekcja 16. Pełny CRUD z bazą danych w EF Core — lista produktów
Trudny / egzaminacyjnyPo co się tego uczymy?
Czas połączyć wszystko z poprzedniej lekcji w jeden, kompletny, DZIAŁAJĄCY przykład: pełny CRUD (Create, Read, Update, Delete) na prawdziwej bazie SQLite przez Entity Framework Core, z walidacją danych przez Data Annotations. To najbardziej praktyczny, powtarzalny wzorzec w całym tym dziale — niemal każda funkcjonalność aplikacji webowej (produkty, zamówienia, kontakty, wpisy na blogu) sprowadza się do tego samego szablonu operacji.
Teoria
Cztery operacje CRUD w Razor Pages z EF Core — każda jako osobna strona:
| Operacja | Strona | Kluczowa metoda EF Core |
|---|---|---|
| Create (utwórz) | Dodaj.cshtml |
context.Produkty.Add(nowy) + SaveChangesAsync() |
| Read (odczytaj) | Lista.cshtml, Szczegoly.cshtml |
ToListAsync(), FindAsync(id) |
| Update (zaktualizuj) | Edytuj.cshtml |
FindAsync(id) → zmiana właściwości → SaveChangesAsync() |
| Delete (usuń) | Usun.cshtml |
context.Produkty.Remove(produkt) + SaveChangesAsync() |
Wzorzec strony edycji — częsta pułapka. Strona edycji ma DWIE metody: OnGetAsync(int id) (wczytuje ISTNIEJĄCY produkt z bazy przez FindAsync(id) i wypełnia nim formularz) oraz OnPostAsync() (odbiera ZMIENIONE dane z formularza, ale musi PONOWNIE pobrać oryginalny obiekt z bazy przez jego Id, ręcznie przepisać do niego nowe wartości pól, i dopiero wtedy wywołać SaveChangesAsync()) — NIE wystarczy po prostu podstawić cały nowy obiekt z formularza w miejsce znalezionego, bo EF Core śledzi KONKRETNĄ instancję obiektu pobraną z bazy.
Strona usuwania z potwierdzeniem. Dobra praktyka UX: usuwanie NIGDY nie powinno wykonywać się od razu po kliknięciu linku (żądanie GET) — zawsze przez osobne żądanie POST z formularza z potwierdzeniem ("Czy na pewno chcesz usunąć?"), żeby przypadkowe kliknięcie linku (albo np. robot wyszukiwarki podążający za wszystkimi linkami na stronie) nie skasowało danych.
Wzorzec pobrania z obsługą "nie znaleziono". var produkt = await context.Produkty.FindAsync(id); if (produkt is null) { return NotFound(); } — ZAWSZE sprawdzaj, czy FindAsync faktycznie coś zwrócił, zanim spróbujesz odczytać jego właściwości; użytkownik mógł wpisać w adresie URL nieistniejące Id (np. /Produkty/Edytuj/9999).
Schemat
Lista.cshtml (READ) Dodaj.cshtml (CREATE)
GET → ToListAsync() GET → pusty formularz
POST → walidacja → Add() → SaveChangesAsync()
│ │
▼ ▼
link "Edytuj" RedirectToPage("Lista")
│
▼
Edytuj.cshtml (UPDATE) Usun.cshtml (DELETE)
GET(id) → FindAsync(id) → wypełnij GET(id) → FindAsync(id) → pokaż potwierdzenie
formularz istniejącymi POST(id) → FindAsync(id) → Remove() →
danymi SaveChangesAsync()
POST → FindAsync(id) → przepisz │
nowe wartości pól → ▼
SaveChangesAsync() RedirectToPage("Lista")
│
▼
RedirectToPage("Lista")
Przykład z życia
Panel zarządzania produktami w sklepie internetowym — dokładnie ten zestaw czterech stron (lista, dodaj, edytuj, usuń) to jeden z najczęściej powtarzających się szablonów w praktyce zawodowej programisty webowego, niezależnie od tego, czy zarządza się produktami, klientami, zamówieniami czy artykułami na blogu.
Pages/Produkty/Edytuj.cshtml + Usun.cshtml
<!-- Pages/Produkty/Edytuj.cshtml -->
@page "{id:int}"
@model EdytujModel
<h1>Edytuj produkt</h1>
<div asp-validation-summary="All" class="text-danger"></div>
<form method="post">
<input type="hidden" asp-for="Produkt.Id" />
<div>
<label asp-for="Produkt.Nazwa"></label>
<input asp-for="Produkt.Nazwa" />
<span asp-validation-for="Produkt.Nazwa" class="text-danger"></span>
</div>
<div>
<label asp-for="Produkt.Cena"></label>
<input asp-for="Produkt.Cena" />
<span asp-validation-for="Produkt.Cena" class="text-danger"></span>
</div>
<button type="submit">Zapisz zmiany</button>
</form>
<!-- Pages/Produkty/Usun.cshtml -->
@page "{id:int}"
@model UsunModel
<h1>Usuń produkt</h1>
<p>Czy na pewno chcesz usunąć produkt "@Model.Produkt.Nazwa"?</p>
<form method="post">
<input type="hidden" asp-for="Produkt.Id" />
<button type="submit">Tak, usuń</button>
<a asp-page="/Produkty/Lista">Anuluj</a>
</form>
Pages/Produkty/Edytuj.cshtml.cs + Usun.cshtml.cs
// Pages/Produkty/Edytuj.cshtml.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MojaStronaApp.Data;
using MojaStronaApp.Modele;
namespace MojaStronaApp.Pages.Produkty;
public class EdytujModel : PageModel
{
private readonly MojaBazaContext _context;
public EdytujModel(MojaBazaContext context) => _context = context;
[BindProperty]
public Produkt Produkt { get; set; } = new();
public async Task<IActionResult> OnGetAsync(int id)
{
var produkt = await _context.Produkty.FindAsync(id);
if (produkt is null) return NotFound();
Produkt = produkt;
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid) return Page();
var produktWBazie = await _context.Produkty.FindAsync(Produkt.Id);
if (produktWBazie is null) return NotFound();
produktWBazie.Nazwa = Produkt.Nazwa;
produktWBazie.Cena = Produkt.Cena;
await _context.SaveChangesAsync();
return RedirectToPage("Lista");
}
}
// ---------------------------------------------------------------------
// Pages/Produkty/Usun.cshtml.cs
public class UsunModel : PageModel
{
private readonly MojaBazaContext _context;
public UsunModel(MojaBazaContext context) => _context = context;
[BindProperty]
public Produkt Produkt { get; set; } = new();
public async Task<IActionResult> OnGetAsync(int id)
{
var produkt = await _context.Produkty.FindAsync(id);
if (produkt is null) return NotFound();
Produkt = produkt;
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
var produkt = await _context.Produkty.FindAsync(Produkt.Id);
if (produkt is not null)
{
_context.Produkty.Remove(produkt);
await _context.SaveChangesAsync();
}
return RedirectToPage("Lista");
}
}
Komentarz i wyjaśnienie kodu
@page "{id:int}" to WAŻNY szczegół routingu Razor Pages — deklaruje, że strona przyjmuje parametr id BEZPOŚREDNIO w adresie URL (np. /Produkty/Edytuj/5, a nie /Produkty/Edytuj?id=5), a :int wymusza, że musi to być liczba całkowita (próba wejścia na /Produkty/Edytuj/abc zwróci 404, zanim w ogóle wejdzie do kodu C#).
W OnPostAsync() metody Edytuj zwróć uwagę na DWA osobne pobrania: Produkt (właściwość [BindProperty], wypełniona danymi z FORMULARZA) i produktWBazie (świeżo pobrany z BAZY przez FindAsync). To rozróżnienie jest konieczne — EF Core śledzi zmiany na obiekcie POBRANYM przez siebie z bazy; nie można po prostu podstawić obiektu z formularza w jego miejsce i oczekiwać, że SaveChangesAsync() zauważy zmiany.
Strona usuwania celowo ma DWIE metody (OnGetAsync pokazuje potwierdzenie, OnPostAsync faktycznie usuwa) — dzięki temu SAMO wejście na adres /Produkty/Usun/5 (żądanie GET, np. przez przypadkowe kliknięcie) NIE usuwa niczego, dopóki użytkownik świadomie nie kliknie przycisku potwierdzającego (żądanie POST).
Ćwiczenie samodzielne
Rozbuduj projekt z lekcji 15 o wszystkie cztery strony CRUD z tej lekcji. Przetestuj pełny cykl: dodaj nowy produkt, sprawdź go na liście, zedytuj jego cenę, sprawdź zmianę na liście, na końcu usuń go (z potwierdzeniem) i sprawdź, że zniknął.
Zadania do pracy własnej
Dodaj do strony
Lista.cshtmllinki "Edytuj" i "Usuń" przy KAŻDYM produkcie, korzystając zasp-route-id="@produkt.Id".Dodaj do strony listy sortowanie (przez query string, np.
?sortuj=cena) korzystające z LINQOrderBy/OrderByDescendingna wyniku pobranym zcontext.Produkty, z linkami w nagłówkach tabeli pozwalającymi przełączać kryterium sortowania.Zbuduj DRUGI, analogiczny CRUD (np. dla modelu
KlientalboZamowienie) całkowicie samodzielnie, bez podglądania przykładu z tej lekcji — z własnym modelem, migracją, i wszystkimi czterema stronami (lista/dodaj/edytuj/usuń), stosując dokładnie te same wzorce (rozróżnienie obiektu z formularza od obiektu z bazy przy edycji, potwierdzenie przy usuwaniu).
Typowe błędy
Wykonywanie usuwania na żądanie GET (np. link bezpośrednio wywołujący usunięcie) — poważny błąd UX i bezpieczeństwa; roboty wyszukiwarek i przeglądarki mogą "przypadkiem" podążyć za linkiem GET (np. prefetching), kasując dane bez świadomej akcji użytkownika.
Próba bezpośredniego przypisania obiektu z formularza do obiektu znalezionego w bazie (produktWBazie = Produkt; zamiast przepisania poszczególnych właściwości) — może prowadzić do nieoczekiwanego zachowania śledzenia zmian EF Core; bezpieczniej jest jawnie przepisać każdą właściwość.
Brak sprawdzenia is null po FindAsync — próba odczytania właściwości z wyniku null (gdy podane Id nie istnieje w bazie) zakończy się wyjątkiem NullReferenceException zamiast eleganckiej odpowiedzi 404.
Nawiązanie do egzaminu zawodowego
Ten wzorzec CRUD to najbardziej uniwersalny, powtarzalny szablon w całym dziale aplikacji webowych — realizuje jednocześnie kilka wymagań INF.04.7.3 (aplikacje korzystające z bazy danych, dynamiczne formularze) i będzie podstawą projektu końcowego — systemu rezerwacji (lekcja 27).