Entity Framework Core to fundament nowoczesnych aplikacji .NET do obsługi baz danych, zapewniający obiektowo-relacyjne mapowanie (ORM) i radykalnie upraszczający dostęp do danych. EF Core składa się z trzech filarów: DbContext, modele danych, system migracji. DbContext jest centralnym punktem zarządzania połączeniem z bazą, modele danych definiują strukturę aplikacji, zaś system migracji pozwala na ewolucyjne zarządzanie schematem bazy. DbContext pełni rolę mostu między obiektami domenowymi a tabelami bazy, śledząc zmiany, realizując zapytania i utrwalając dane. Modele (POCO) są mapowane na tabele zgodnie z konwencjami frameworku. Migracje umożliwiają przyrostowe oraz powtarzalne dostosowanie schematu bazy do zmian w modelu danych. Integracja tych elementów eliminuje konieczność ręcznego pisania złożonego kodu dostępu do danych.
Wprowadzenie do Entity Framework Core
Entity Framework Core to nowoczesna, wieloplatformowa i otwarta wersja ORM od Microsoft, z obsługą .NET Core i .NET 5+. Pozwala on programistom .NET na pracę z bazami danych za pomocą obiektów, bez konieczności samodzielnego tworzenia większości kodu SQL.
Architektura EF Core bazuje na kilku kluczowych konceptach współdziałających dla płynnego dostępu do danych. Sercem tego systemu jest DbContext, reprezentujący sesję z bazą – pozwala on na wykonywanie zapytań i zapisywanie danych. Model danych obejmuje klasy jednostek i kontekst, gdzie klasy odpowiadają tabelom, a ich właściwości – kolumnom. Framework umożliwia zarówno generowanie modelu z istniejącej bazy, jak i ręczne kodowanie modelu aplikacji.
Przez system dostawców baz danych (Database Providers) EF Core wspiera wiele silników baz danych, bez konieczności zmiany logiki dostępu do danych. Wśród najpopularniejszych dostawców są:
- SQL Server,
- SQLite,
- PostgreSQL,
- MySQL,
- wiele innych rozwiązań.
Mechanizm śledzenia zmian (Change Tracking) automatycznie monitoruje modyfikacje obiektów, realizując odpowiednie polecenia SQL przy synchronizacji z bazą. Programista skupia się na logice biznesowej, a framework optymalizuje zapytania SQL i zarządza połączeniami.
System konfiguracji EF Core pozwala na dostosowanie działania frameworku poprzez:
- konwencje,
- adnotacje danych (Data Annotations),
- Fluent API.
Ta elastyczność sprawia, że EF Core sprawdza się zarówno w prostych aplikacjach CRUD, jak i dużych systemach enterprise.
DbContext – serce Entity Framework Core
DbContext jest główną klasą obsługującą interakcje aplikacji z bazą danych. Służy jako most do świata relacyjnego, zarządza połączeniem, śledzi zmiany i tworzy oraz wykonuje SQL. Każda instancja DbContext to jednostka pracy (unit of work) przeznaczona zwykle na jedno żądanie lub operację.
Życie DbContext kończy się wraz z jego utylizacją. Najczęściej modeluje się go jako krótkożyjący, np. jedna instancja na żądanie HTTP w aplikacji ASP.NET Core, co zapewnia izolację danych między zapytaniami oraz lepsze zarządzanie zasobami.
Ta architektura rozwiązuje problem współdzielonego stanu i zwiększa bezpieczeństwo wielowątkowe.
DbContext można skonfigurować na różne sposoby. Standardowo używa się metody OnConfiguring klasy pochodnej DbContext:
public class ApplicationDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=Test;ConnectRetryCount=0");
}
}
Alternatywnie konfiguracja może być przekazywana przez konstruktor, co jest niezbędne przy dependency injection. Wzorzec ten oddziela konfigurację od implementacji i ułatwia testowanie oraz dynamiczne ustawienia środowiskowe.
DbSet<T> jest kluczowym składnikiem DbContext, reprezentującym kolekcję encji danego typu i odwzorowywanym na konkretne tabele. Przykład:
public class CompanyContext : DbContext
{
public DbSet<Department> Department { get; set; }
public DbSet<Employee> Employee { get; set; }
public CompanyContext(DbContextOptions<CompanyContext> options) : base(options) { }
}
DbContext automatycznie śledzi stan encji (Added, Modified, Deleted, Unchanged, Detached), co pozwala na automatyczne generowanie SQL przy wywołaniu SaveChanges(). Deweloper manipuluje obiektami, a framework przekłada to na konkretne operacje bazodanowe.
DbContext obsługuje także transakcje oraz oferuje mechanizmy optymalizacyjne, jak Connection Pooling czy Lazy Loading. Automatyczne zarządzanie transakcjami w SaveChanges() gwarantuje spójność danych, a przy potrzebie większej kontroli jest dostępne API do manualnej obsługi transakcji i wydajności.
Modele danych w Entity Framework Core
Modelowanie danych polega na definiowaniu klas C#, które są mapowane na tabele bazodanowe, a ich właściwości odwzorowują kolumny. Dzięki konwencjom frameworku praktycznie każda klasa encji automatycznie staje się tabelą.
Podstawę modelu stanowią klasy encji, tzw. POCO. Przykład prostych encji dla aplikacji książkowej:
public class Author
{
public int AuthorId { get; set; }
public string LastName { get; set; }
public List<Book> Titles { get; set; } = new List<Book>();
}
public class Book
{
public int BookId { get; set; }
public string Title { get; set; }
public Author Author { get; set; }
}
Framework wspiera różne style modelowania:
- Code First – model jest tworzony kodem i na jego podstawie generowana jest baza,
- Database First – model generowany jest z istniejącej bazy danych,
- Model First – (rzadziej spotykany) najpierw powstaje model, potem baza.
Code First daje największą kontrolę nad strukturą danych oraz łatwość ewolucji wraz z rozwojem aplikacji. Relacje odwzorowuje się dzięki navigation properties. Na przykład, relacja jeden-do-wielu (Grade–Student):
public class Grade
{
public int GradeId { get; set; }
public string GradeName { get; set; }
public string Section { get; set; }
public ICollection<Student> Students { get; set; }
}
public class Student
{
public int StudentId { get; set; }
public string StudentName { get; set; }
public Grade Grade { get; set; }
}
Entity Framework automatycznie stosuje konwencje:
- rozpoznawanie kluczy głównych (
Idlub{ClassName}Id), - dobór typów danych na podstawie typu właściwości C#,
- identyfikacja relacji przez navigation properties,
- mapowanie klas i właściwości na tabele/kolumny.
Zaawansowana konfiguracja modelu wykonywana jest w metodzie OnModelCreating poprzez Fluent API, umożliwiającą precyzyjne dostrojenie odwzorowań oraz własnych ograniczeń, indeksów itp.
Migracje – zarządzanie schematem bazy danych
Migracje w Entity Framework Core pozwalają na płynne, kontrolowane i powtarzalne zmiany struktury bazy danych w tempie rozwoju modelu aplikacji. W trybie Code First są nieocenionym narzędziem synchonizującym schemat kodu i bazy danych.
Migracje bazują na migawkach (snapshots), wykrywając różnice między kolejnymi wersjami modelu. Po modyfikacjach klas domenowych wywołuje się migrację, która generuje kod odpowiedzialny za dostosowanie bazy:
- dotnet ef migrations add InitialCreate – dla .NET CLI,
- Add-Migration InitialCreate – dla Package Manager Console w Visual Studio.
Procedura generuje katalog Migrations z klasą migracji i plikiem snapshot oraz metodami Up() (tworzenie struktur) i Down() (wycofanie zmian).
Przykład wygenerowanej migracji:
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Students",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Students", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(name: "Students");
}
}
Aktualizacja bazy:
- dotnet ef database update – stosowane jest przez .NET CLI,
- Update-Database – polecenie dla Package Manager Console.
Framework prowadzi historię zastosowanych migracji w tabeli __EFMigrationsHistory, by stosować tylko nowe migracje.
Podstawowe polecenia CLI do migracji obejmują:
add– dodawanie nowej migracji,list– listowanie migracji,remove– usuwanie ostatniej migracji,script– generowanie skryptu SQL.
W środowisku produkcyjnym migracje zaleca się wdrażać przez generowanie i ręczne stosowanie skryptów SQL. Skrypt generuje się poleceniem:
dotnet ef migrations script
To daje kontrolę nad wdrożeniem oraz integrację z procesami change management.
Praktyczna implementacja od podstaw
Aby wdrożyć EF Core praktycznie, zacznij od przygotowania projektu i instalacji wymaganych paczek:
- utworzenie projektu konsolowego .NET (
dotnet new console -o EFGetStarted), - zainstalowanie dostawcy bazy, np. Microsoft.EntityFrameworkCore.Sqlite (
dotnet add package Microsoft.EntityFrameworkCore.Sqlite), - doinstalowanie narzędzi deweloperskich Microsoft.EntityFrameworkCore.Tools (
Install-Package Microsoft.EntityFrameworkCore.Tools).
Model danych – na przykładzie bloga – będzie wyglądał następująco:
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public int Rating { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
Definicja kontekstu:
public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Data Source=blogging.db");
}
}
Kolejne kroki prac z bazą obejmują:
- dodanie migracji (
dotnet ef migrations add InitialCreate), - zastosowanie migracji (
dotnet ef database update).
CRUD – operacje na danych (Create, Read, Update, Delete):
using var db = new BloggingContext();
// Create
db.Add(new Blog { Url = "http://blogs.msdn.com/adonet" });
await db.SaveChangesAsync();
// Read
var blog = await db.Blogs.OrderBy(b => b.BlogId).FirstAsync();
// Update
blog.Url = "https://devblogs.microsoft.com/dotnet";
blog.Posts.Add(new Post { Title = "Hello World", Content = "I wrote an app using EF Core!" });
await db.SaveChangesAsync();
// Delete
db.Remove(blog);
await db.SaveChangesAsync();
Cyklem życia DbContext w aplikacjach konsolowych zarządza się przez using statement. W ASP.NET Core DbContext rejestruje się jako scoped service, co automatycznie zarządza cyklem życia w ramach żądania HTTP.
Zaawansowane aspekty konfiguracji
Zaawansowana konfiguracja EF Core obejmuje precyzyjne mapowanie O/RM oraz pełną kontrolę nad frameworkiem. Dostępne są trzy poziomy konfiguracji:
- konwencje,
- adnotacje danych,
- Fluent API.
Fluent API dostępny w OnModelCreating pozwala konfigurować własności encji, relacje, indeksy, mapowania na tabele i kolumny:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>(entity =>
{
entity.HasKey(e => e.StudentId);
entity.Property(e => e.FirstName).IsRequired().HasMaxLength(50);
entity.Property(e => e.Email).HasMaxLength(100).IsUnique();
entity.HasIndex(e => e.Email);
});
modelBuilder.Entity<Student>()
.HasOne(s => s.Branch)
.WithMany(b => b.Students)
.HasForeignKey(s => s.BranchId)
.OnDelete(DeleteBehavior.Cascade);
}
Rejestracja DbContext i konfiguracja połączenia w ASP.NET Core przez dependency injection umożliwia dynamiczną kontrolę środowiskową i cyklu życia:
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
Framework sam zarządza instancjami oraz izolacją wątków dla każdego żądania HTTP.
EF Core wspiera różne strategie dziedziczenia:
- Table-per-Hierarchy (TPH),
- Table-per-Type (TPT),
- Table-per-Concrete-Type (TPC).
Przykład TPH:
modelBuilder.Entity<Person>()
.HasDiscriminator<string>("PersonType")
.HasValue<Student>("Student")
.HasValue<Teacher>("Teacher");
Mechanizmy optymalizacji obejmują Connection Pooling, Change Tracking optimization, Query Splitting. Connection Pooling aktywuje się:
builder.Services.AddDbContextPool<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString), poolSize: 32);
Strategie ładowania danych:
- Lazy Loading,
- Eager Loading,
- Explicit Loading.
Lazy Loading globalnie przez:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLazyLoadingProxies();
}
Dla produkcji migracje powinny być wdrażane przez kontrolowane skrypty SQL:
dotnet ef migrations script --idempotent --output migration.sql
Flagę --idempotent stosujemy, by zapewnić bezpieczeństwo powtarzalnych wdrożeń.
Najlepsze praktyki i optymalizacje
Efektywne użycie EF Core oznacza stosowanie najlepszych praktyk zapewniających wydajność, skalowalność i bezpieczeństwo. DbContext powinien być krótkotrwały i ograniczony do jednej jednostki pracy (np. żądanie HTTP). W ASP.NET Core DbContext rejestruje się jako scoped service.
Optymalizację zapytań zapewnia:
- projection (Select) – pobieranie tylko niezbędnych kolumn,
- rozsądne użycie Eager Loading (Include),
- AsNoTracking dla zapytań tylko do odczytu.
Przykład Projection + AsNoTracking:
var blogTitles = await context.Blogs
.Where(b => b.Rating > 3)
.Select(b => new { b.BlogId, b.Title })
.ToListAsync();
var blogs = await context.Blogs
.AsNoTracking()
.Where(b => b.IsPublished)
.ToListAsync();
Connection Pooling skraca czas tworzenia kontekstu i podnosi wydajność:
services.AddDbContextPool<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString), poolSize: 128);
DbContext oraz EF Core automatycznie tworzą indeksy dla kluczy głównych i obcych, pozostałe należy skonfigurować ręcznie przez Fluent API:
modelBuilder.Entity<Product>()
.HasIndex(p => p.Name)
.HasDatabaseName("IX_Product_Name");
modelBuilder.Entity<Product>()
.HasIndex(p => new { p.CategoryId, p.Price })
.HasDatabaseName("IX_Product_Category_Price");
Framework chroni przed SQL Injection poprzez parametryzację zapytań, jednak przy raw SQL należy stosować parametry:
var blogs = context.Blogs
.FromSqlRaw("SELECT * FROM Blogs WHERE Rating > {0}", minRating)
.ToList();
DbContext automatycznie obsługuje transakcje w SaveChanges(). Dla zaawansowanych scenariuszy – manualna obsługa transakcji:
using var transaction = context.Database.BeginTransaction();
try
{
context.Blogs.Add(newBlog);
await context.SaveChangesAsync();
await externalService.NotifyAsync(newBlog.Id);
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
EF Core integruje się z systemem logowania ASP.NET Core. Ustawienie poziomu logowania oraz logowania wrażliwych informacji:
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging());
Najlepsze praktyki testowania: testy jednostkowe na bazie In-Memory, testy integracyjne na wydzielonych bazach (np. Docker). Konfiguracja na potrzeby testów jednostkowych:
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: "TestDatabase")
.Options;
using var context = new ApplicationDbContext(options);