Dataczwartek, 13 sierpnia 2026 Czas18:43:33
← Aplikacje webowe

Lekcja 16. Pełny CRUD z bazą danych w EF Core — lista produktów

Trudny / egzaminacyjny

Po 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

  1. Dodaj do strony Lista.cshtml linki "Edytuj" i "Usuń" przy KAŻDYM produkcie, korzystając z asp-route-id="@produkt.Id".

  2. Dodaj do strony listy sortowanie (przez query string, np. ?sortuj=cena) korzystające z LINQ OrderBy/OrderByDescending na wyniku pobranym z context.Produkty, z linkami w nagłówkach tabeli pozwalającymi przełączać kryterium sortowania.

  3. Zbuduj DRUGI, analogiczny CRUD (np. dla modelu Klient albo Zamowienie) 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).