Entity Framework Core w ASP.NET — konfiguracja od zera

Jak wpiąć Entity Framework Core do aplikacji ASP.NET Core MVC — DbContext, connection string, migracje i pierwszy zapis do bazy. Krok po kroku, bez magii.

JAJarosław Armatys 8 min czytania
Wersja wideo tego wpisu — obejrzyj na YouTube

Przyszedł moment, w którym nasza aplikacja z kursu przestaje być tylko "widokiem z formularzem", a zaczyna trzymać dane w bazie. W tym wpisie przechodzimy krok po kroku przez konfigurację Entity Framework Core w ASP.NET Core MVC — od instalacji paczek, przez DbContext, aż po pierwszą migrację i zapis produktu do SQL Servera.

Jeśli wolisz zerknąć od razu na kod, cały projekt do tej lekcji masz tutaj: github.com/jarmatys/asp-net-core-podstawy (Lekcja-6).

Po co Entity Framework

Mógłbyś pisać zapytania SQL ręcznie, ClassLibrary z SqlConnection, SqlCommand, ręczne mapowanie wyników na obiekty… i po tygodniu mieć dość. EF Core to ORM — pozwala Ci myśleć o danych w kategoriach klas i obiektów, a nie tabel i kursorów.

Dorzucasz model Product, mówisz "to jest tabela", odpalasz migrację i EF Core sam generuje CREATE TABLE, sam wyciąga dane, sam aktualizuje. Oczywiście to nie jest magia — pod spodem i tak leci SQL, tylko Ty go nie piszesz. W zamian dostajesz:

  • mapowanie modeli na tabele (konwencja nad konfiguracją),
  • migracje, czyli wersjonowanie schematu bazy,
  • LINQ jako sposób pisania zapytań,
  • śledzenie zmian (change tracking) — nie musisz pamiętać, co zmieniłeś, EF to ogarnia.

W naszym kursie używam EF Core 3.1, bo cała aplikacja leci na .NET Core 3.1. Jeśli startujesz od zera w nowszym .NET, weź najnowszą wersję EF Core zgodną z Twoim targetem — składnia, którą tu pokazuję, jest kompatybilna.

Pakiety NuGet i connection string

Zaczynamy od trzech paczek. W Visual Studio otwieramy Manage NuGet Packages for Solution i szukamy entity:

  • Microsoft.EntityFrameworkCore — core biblioteki,
  • Microsoft.EntityFrameworkCore.Tools — to dzięki temu pakietowi działają komendy Add-Migration i Update-Database w Package Manager Console,
  • Microsoft.EntityFrameworkCore.SqlServer — provider pod SQL Server (będziemy jechać na LocalDB).

Wszystkie w tej samej wersji, zgodnej z Twoim .NET. U mnie w kursie to 3.1.13.

Teraz connection string. Wrzucamy go do appsettings.json, żeby nie trzymać go w kodzie:

// appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=KursDb;Trusted_Connection=True"
  }
}

Dwa ukośniki \\ to nie przypadek — w JSON-ie musisz je zescape'ować. Jeśli dostaniesz ostrzeżenie od Visual Studio o znakach specjalnych w stringu w kodzie C#, użyj @"..." (verbatim string) — wtedy nie musisz eskejpować backslasha.

LocalDB jest wygodny, bo jedzie lokalnie na Twoim laptopie bez stawiania pełnego SQL Servera. Do nauki i do małych projektów w zupełności wystarcza.

DbContext i DbSet

DbContext to serce EF Core. To klasa, przez którą rozmawiasz z bazą. Każda właściwość typu DbSet<T> odpowiada jednej tabeli.

Robimy katalog Data (albo u mnie w kursie bezpośrednio w projekcie) i dodajemy klasę AppDbContext:

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
    public DbSet<Product> Products { get; set; }
}

Co tu się dzieje:

  • dziedziczymy po DbContext z Microsoft.EntityFrameworkCore,
  • konstruktor przyjmuje DbContextOptions<AppDbContext> i przekazuje do klasy bazowej — dzięki temu za chwilę w Startup.cs wstrzykniemy tu connection string i provider,
  • DbSet<Product> Products — ta nazwa (Products) stanie się nazwą tabeli w bazie. EF Core pluralizuje nazwy modeli konwencją, więc jeżeli Twoja klasa to Product, DbSet o nazwie Products da Ci tabelę Products. Proste.

W modelu Product wypada jeszcze oznaczyć klucz główny. Jeśli masz właściwość Id, EF Core rozpozna ją automatycznie, ale dla porządku często dodaję atrybut jawnie:

public class Product
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public string Category { get; set; }
}

[Key] wymaga using System.ComponentModel.DataAnnotations;. Id typu int staje się autoinkrementowalnym kluczem — bazie wygenerujemy go przy pierwszym Save.

Jeden DbContext per logiczna baza danych. W kursie mamy jeden, ale w większych projektach czasem widuje się dzielenie kontekstów per moduł. Na start trzymaj się jednego — łatwiej debugować migracje.

Teraz rejestracja w Startup.cs, metoda ConfigureServices:

// Startup.ConfigureServices
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

AddDbContext wrzuca AppDbContext do kontenera DI ze scopem Scoped (jeden per request HTTP). UseSqlServer to metoda z paczki Microsoft.EntityFrameworkCore.SqlServer — mówi EF-owi, że gadamy z SQL Serverem. Configuration.GetConnectionString("DefaultConnection") czyta sekcję ConnectionStrings:DefaultConnection z appsettings.json.

Od tego momentu każdy kontroler i serwis może sobie w konstruktorze poprosić o AppDbContext i EF go dostarczy.

Migracje i aktualizacja bazy

Schemat bazy powstaje z Twoich modeli. Ale ktoś musi go wygenerować — to robią migracje.

Otwierasz Package Manager Console (View → Other Windows → Package Manager Console), upewniasz się, że Default Project to Twój projekt z AppDbContext, i odpalasz:

Add-Migration Init
Update-Database

Add-Migration Init:

  • skanuje Twój DbContext i wszystkie DbSet<T>,
  • generuje folder Migrations/ z klasą YYYYMMDDhhmmss_Init.cs,
  • w tej klasie znajdziesz metody Up() (co zrobić przy aplikacji migracji) i Down() (jak cofnąć — przydatne przy rewertach),
  • w Up() zobaczysz migrationBuilder.CreateTable("Products", ...) z kolumnami odpowiadającymi Twojemu modelowi.

Migracja to tylko plan. Baza wciąż nie istnieje. Do jej utworzenia i zastosowania migracji służy:

Update-Database — EF Core łączy się z SQL Serverem po connection stringu, jeżeli baza KursDb nie istnieje, to ją utworzy, a potem odpala Up() wszystkich niezaaplikowanych migracji.

Po udanym Update-Database odśwież SQL Server Object Explorer w Visual Studio. Zobaczysz:

  • tabelę Products z kolumnami Id, Name, Description, Category,
  • tabelę __EFMigrationsHistory — tu EF zapisuje, które migracje już zaaplikował. Dzięki temu Update-Database nie próbuje tej samej migracji wrzucić dwa razy.

Nie edytuj plików migracji ręcznie, chyba że wiesz co robisz. Jeżeli zmienisz model, odpalasz kolejną migrację (Add-Migration DodaniePola) i znowu Update-Database. EF policzy diff i dopisze migrację inkrementalnie.

Pierwszy zapis i odczyt

Mamy bazę, mamy kontekst, mamy tabelę. Czas coś zapisać. W serwisie, który wstrzykujemy do kontrolera (u nas WarehouseService), bierzemy AppDbContext przez konstruktor:

public class WarehouseService : IWarehouseService
{
    private readonly AppDbContext _dbContext;

    public WarehouseService(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public int AddProduct(Product product)
    {
        _dbContext.Products.Add(product);
        return _dbContext.SaveChanges();
    }

    public List<Product> GetProducts()
    {
        return _dbContext.Products.ToList();
    }
}

Co warto wiedzieć:

  • _dbContext.Products.Add(product) tylko oznacza obiekt jako "do wstawienia". SQL jeszcze nie leci.
  • SaveChanges() dopiero wysyła INSERT do bazy. Zwraca liczbę zmodyfikowanych rekordów — przydatne do sprawdzenia, czy zapis się udał.
  • Po SaveChanges() EF Core aktualizuje obiekt product o wartość Id wygenerowaną przez bazę. Więc jeżeli wołasz AddProduct(product), to po wyjściu z metody product.Id będzie już miał właściwą wartość — nie trzeba robić dodatkowego SELECT.
  • ToList() materializuje zapytanie. Póki piszesz _dbContext.Products.Where(...), to jeszcze IQueryable — zapytanie nie poszło. Dopiero ToList(), FirstOrDefault() albo foreach je odpala.

W kontrolerze, po walidacji ModelState.IsValid, wywołujesz _warehouseService.AddProduct(product) i gotowe — masz produkt w bazie, a zwrócony Id możesz pokazać userowi albo użyć do redirectu.

Co dalej?

Masz już pełny pipeline: model → DbContext → migracja → baza → zapis → odczyt. To jest fundament, na którym zbudujesz całą warstwę danych aplikacji. W kolejnej lekcji kursu wchodzimy głębiej w EF Core — pobieranie pojedynczego rekordu po Id, filtrowanie, aktualizacje, usuwanie. Tam się zaczyna prawdziwa zabawa z LINQ.

Kod do tej lekcji: github.com/jarmatys/asp-net-core-podstawy (Lekcja-6) — sklonuj, odpal Update-Database i zobacz, jak to działa u siebie. Jak coś nie pyka, zostaw komentarz pod filmem albo napisz przez armatys.me — rozgryziemy.

Tagi:#course-aspnet#dotnet#entity-framework#database

Masz pytanie do tego wpisu?

Napisz do mnie — chętnie porozmawiam o szczegółach technicznych, albo o tym jak zastosować to u Ciebie w projekcie.