Lekcja 15. Entity Framework Core — wprowadzenie do bazy danych w ASP.NET Core
Trudny / egzaminacyjnyPo 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
Dodaj do modelu
Produktnową 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.Napisz stronę Razor Pages, która przy starcie (
OnGetAsync) sprawdza, czy tabelaProduktyjest pusta, i jeśli tak — dodaje kilka przykładowych produktów przezcontext.Produkty.AddRange(...)+SaveChangesAsync()(tzw. "seed danych"), a następnie wyświetla pełną listę.Zbuduj kompletną stronę CRUD dla modelu
Produktprzez EF Core: listę (SELECT), formularz dodawania (INSERT, z walidacją Data Annotations), formularz edycji (UPDATE — pobranie przezFindAsync, 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.