Dataniedziela, 9 sierpnia 2026 Czas07:18:18
← Aplikacje webowe

Lekcja 15. Entity Framework Core — wprowadzenie do bazy danych w ASP.NET Core

Trudny / egzaminacyjny

Po co się tego uczymy?

Dotychczasowe przykłady w tym dziale trzymały dane "na sztywno" w pamięci (listy w kodzie C#) — znikające przy każdym restarcie aplikacji. Prawdziwa aplikacja webowa potrzebuje trwałej bazy danych. Entity Framework Core (EF Core) to oficjalne narzędzie Microsoftu (ORM — Object-Relational Mapper) pozwalające pracować z bazą danych przez zwykłe obiekty i klasy C#, zamiast pisać ręczne zapytania SQL za każdym razem — podobnie jak sqlite-net-pcl w dziale MAUI, ale znacznie bardziej rozbudowane.

Teoria

ORM (Object-Relational Mapper) to warstwa tłumacząca między światem OBIEKTÓW C# (klasy, właściwości, listy) a światem RELACYJNEJ bazy danych (tabele, wiersze, kolumny, klucze obce) — piszesz kod w C#, a EF Core w tle generuje i wykonuje odpowiednie zapytania SQL. To nie zwalnia z rozumienia SQL (dział "Bazy danych i SQL" pozostaje fundamentem), ale znacznie przyspiesza codzienną pracę i redukuje liczbę błędów wynikających z ręcznego sklejania zapytań.

DbContext — centralna klasa EF Core, reprezentująca POŁĄCZENIE z bazą danych i ZBIÓR wszystkich tabel dostępnych w aplikacji. Każda właściwość typu DbSet<T> odpowiada JEDNEJ tabeli w bazie — np. DbSet<Produkt> Produkty { get; set; } reprezentuje tabelę Produkty, gdzie każdy wiersz to jeden obiekt klasy Produkt.

Podejście Code First. Kurs bitedu.pl uczy podejścia "Code First" — NAJPIERW piszesz klasy C# (modele), a EF Core na ich podstawie TWORZY strukturę bazy danych (tabele, kolumny, klucze) — to odwrotność podejścia "Database First", gdzie baza już istnieje, a klasy generuje się z niej automatycznie.

Migracje — kontrola wersji struktury bazy danych. Zamiast ręcznie pisać CREATE TABLE, EF Core generuje migracje — pliki C# opisujące ZMIANY w strukturze bazy (dodanie tabeli, dodanie kolumny), które można zastosować (Update-Database) albo cofnąć. Podstawowe polecenia (w Konsoli Menedżera Pakietów w Visual Studio albo dotnet ef w terminalu):

Polecenie Działanie
Add-Migration NazwaMigracji tworzy nowy plik migracji na podstawie zmian w klasach modeli od ostatniej migracji
Update-Database stosuje WSZYSTKIE niezastosowane jeszcze migracje — faktycznie tworzy/zmienia tabele w bazie
Remove-Migration usuwa OSTATNIĄ, jeszcze niezastosowaną migrację (np. gdy zauważysz w niej błąd przed jej zastosowaniem)

Connection string (ciąg połączenia z bazą danych) zapisuje się w appsettings.json, a rejestruje w Program.cs: builder.Services.AddDbContext<MojaBazaContext>(options => options.UseSqlite(connectionString)). Kurs bitedu.pl (konsekwentnie z resztą działów) korzysta z SQLite jako silnika bazy — ten sam lekki, bezserwerowy silnik, którego uczyłeś się w dziale MAUI, tylko teraz osadzony w aplikacji webowej zamiast mobilnej.

Podstawowe operacje (CRUD) przez DbContext: context.Produkty.ToListAsync() (SELECT wszystkich wierszy), context.Produkty.Add(nowyProdukt) + await context.SaveChangesAsync() (INSERT), context.Produkty.FindAsync(id) (SELECT po kluczu głównym), zmiana właściwości pobranego obiektu + SaveChangesAsync() (UPDATE), context.Produkty.Remove(produkt) + SaveChangesAsync() (DELETE) — kluczowa zasada: żadna zmiana NIE trafia do bazy, dopóki nie wywołasz SaveChangesAsync(), EF Core śledzi zmiany w pamięci i zapisuje je wszystkie naraz dopiero na to wywołanie.

Schemat

Modele/Produkt.cs (klasa C#)
        │
        │  Code First: klasa OPISUJE strukturę tabeli
        ▼
Data/MojaBazaContext.cs (DbContext)
   DbSet<Produkt> Produkty { get; set; }
        │
        │  Add-Migration InitialCreate
        ▼
Migrations/20260708_InitialCreate.cs (wygenerowany opis zmian)
        │
        │  Update-Database
        ▼
Rzeczywista tabela "Produkty" w pliku bazy SQLite

Operacje w kodzie (PageModel/Controller):
context.Produkty.ToListAsync()          → SELECT * FROM Produkty
context.Produkty.Add(p) + SaveChangesAsync()  → INSERT
context.Produkty.FindAsync(id)          → SELECT ... WHERE Id = @id
zmiana właściwości + SaveChangesAsync() → UPDATE
context.Produkty.Remove(p) + SaveChangesAsync() → DELETE

Przykład z życia

Sklep internetowy przechowujący tysiące produktów potrzebuje prawdziwej bazy danych, a nie listy w pamięci znikającej przy każdym restarcie serwera — Entity Framework Core pozwala zarządzać taką bazą przez wygodne, obiektowe API w C#, zachowując jednocześnie pełną moc relacyjnej bazy danych pod spodem (te same tabele, klucze, zapytania SQL, które poznałeś w dziale "Bazy danych i SQL").

Modele/Produkt.cs + Data/MojaBazaContext.cs + Program.cs

// Modele/Produkt.cs
namespace MojaStronaApp.Modele;

public class Produkt
{
    public int Id { get; set; }
    public string Nazwa { get; set; } = "";
    public double Cena { get; set; }
}

// ---------------------------------------------------------------------

// Data/MojaBazaContext.cs
using Microsoft.EntityFrameworkCore;
using MojaStronaApp.Modele;

namespace MojaStronaApp.Data;

public class MojaBazaContext : DbContext
{
    public MojaBazaContext(DbContextOptions<MojaBazaContext> options) : base(options)
    {
    }

    public DbSet<Produkt> Produkty { get; set; }
}

// ---------------------------------------------------------------------

// Program.cs - rejestracja DbContext
using Microsoft.EntityFrameworkCore;
using MojaStronaApp.Data;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

string connectionString = builder.Configuration.GetConnectionString("MojaBaza")
    ?? "Data Source=mojabaza.db";
builder.Services.AddDbContext<MojaBazaContext>(options =>
    options.UseSqlite(connectionString));

var app = builder.Build();
// ... reszta konfiguracji jak w poprzednich lekcjach
app.Run();

appsettings.json + polecenia migracji

// appsettings.json - connection string
{
  "ConnectionStrings": {
    "MojaBaza": "Data Source=mojabaza.db"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

// ---------------------------------------------------------------------

// Terminal - polecenia migracji (wymaga zainstalowanego dotnet-ef)
// dotnet tool install --global dotnet-ef
// dotnet ef migrations add InitialCreate
// dotnet ef database update

// Przykładowa strona odczytująca dane przez DbContext:
// Pages/Produkty/Lista.cshtml.cs
//
// public class ListaModel : PageModel
// {
//     private readonly MojaBazaContext _context;
//     public ListaModel(MojaBazaContext context) => _context = context;
//
//     public List<Produkt> Produkty { get; set; } = new();
//
//     public async Task OnGetAsync()
//     {
//         Produkty = await _context.Produkty.ToListAsync();
//     }
// }

Komentarz i wyjaśnienie kodu

MojaBazaContext dziedziczy po DbContext (klasa bazowa EF Core) i przyjmuje w konstruktorze DbContextOptions<MojaBazaContext> — to standardowy wzorzec Dependency Injection, identyczny mechanizm jak SignInManager w poprzedniej lekcji; ASP.NET Core sam dostarczy odpowiednio skonfigurowaną instancję, korzystając z ustawień zarejestrowanych w Program.cs (AddDbContext).

builder.Configuration.GetConnectionString("MojaBaza") odczytuje connection string z pliku appsettings.json — trzymanie go w pliku konfiguracyjnym (zamiast na sztywno w kodzie) pozwala mieć RÓŻNE connection stringi dla środowiska deweloperskiego i produkcyjnego, bez zmiany kodu aplikacji.

Operator ?? w connectionString = ... ?? "Data Source=mojabaza.db" zapewnia wartość domyślną, gdyby klucz w konfiguracji nie istniał — dokładnie ten sam wzorzec bezpiecznego dostępu, który znasz z lekcji o modelach i Preferences w dziale MAUI.

Ćwiczenie samodzielne

W istniejącym projekcie ASP.NET Core zainstaluj pakiety NuGet Microsoft.EntityFrameworkCore.Sqlite i Microsoft.EntityFrameworkCore.Design, dodaj klasę modelu i DbContext jak w przykładzie, zarejestruj go w Program.cs, i wykonaj pierwszą migrację (Add-Migration + Update-Database w Konsoli Menedżera Pakietów, albo odpowiedniki dotnet ef w terminalu). Sprawdź (np. w DB Browser for SQLite), że plik bazy z tabelą faktycznie powstał.

Zadania do pracy własnej

  1. Dodaj do modelu Produkt nową właściwość Opis (string), wygeneruj nową migrację (Add-Migration DodanieOpisu) i zastosuj ją (Update-Database) — sprawdź, że istniejąca tabela w bazie faktycznie zyskała nową kolumnę, BEZ utraty wcześniej zapisanych danych.

  2. Napisz stronę Razor Pages, która przy starcie (OnGetAsync) sprawdza, czy tabela Produkty jest pusta, i jeśli tak — dodaje kilka przykładowych produktów przez context.Produkty.AddRange(...) + SaveChangesAsync() (tzw. "seed danych"), a następnie wyświetla pełną listę.

  3. Zbuduj kompletną stronę CRUD dla modelu Produkt przez EF Core: listę (SELECT), formularz dodawania (INSERT, z walidacją Data Annotations), formularz edycji (UPDATE — pobranie przez FindAsync, zmiana właściwości, SaveChangesAsync) i usuwanie (DELETE) — wszystko operujące na PRAWDZIWEJ bazie SQLite, bez żadnych danych "na sztywno" w pamięci.

Typowe błędy

Zapominanie o await context.SaveChangesAsync() po Add/Remove albo zmianie właściwości — bez tego wywołania ŻADNA zmiana nie trafia faktycznie do bazy danych, mimo że kod C# "wygląda" na poprawny.

Ręczne pisanie zapytań SQL zamiast korzystania z DbSet<T> tam, gdzie EF Core w pełni wystarcza — traci się wtedy główną zaletę ORM (bezpieczeństwo typów, ochronę przed SQL Injection wbudowaną w mechanizm parametryzacji zapytań LINQ).

Zapominanie o wykonaniu migracji po zmianie modelu — jeśli dodasz właściwość do klasy Produkt, ale nie wygenerujesz i nie zastosujesz nowej migracji, baza danych i kod C# "rozjadą się", prowadząc do wyjątków przy próbie odczytu/zapisu tej nowej kolumny.

Nawiązanie do egzaminu zawodowego

To bezpośrednia realizacja INF.04.7.3 "aplikacje korzystające z bazy danych" oraz naturalne połączenie z całym działem "Bazy danych i SQL" (INF.03.4) — EF Core to praktyczne, przemysłowe narzędzie budowane NA BAZIE tej samej wiedzy o tabelach, kluczach i normalizacji. Kolejna lekcja pokazuje pełny, praktyczny projekt CRUD wykorzystujący EF Core od początku do końca.