- Писать классы модели и
DbContext, создавать и применять миграции - Читать, добавлять, изменять и удалять данные через LINQ
- Настраивать связи и избегать проблемы N+1 и SQL-инъекций
С низкоуровневым ADO.NET каждый SQL-запрос приходится писать вручную и переносить каждый столбец в объект. Entity Framework Core (EF Core), официальная ORM для .NET, берёт эту работу на себя: ты пишешь классы, EF Core создаёт для них таблицы, переводит запросы LINQ в SQL и собирает результаты в объекты. EF Core работает с SQL Server, PostgreSQL, MySQL и SQLite; в примерах мы используем SQLite, который не требует установки.
- 1Добавь пакеты
Microsoft.EntityFrameworkCore.Sqlite— провайдер SQLite, аMicrosoft.EntityFrameworkCore.Designнужен инструментам миграций. - 2Установи инструмент dotnet-ef
dotnet tool install --global dotnet-efдобавляет командыdotnet ef; достаточно сделать это один раз на компьютере. - 3Напиши модель и DbContext
Каждая таблица — это класс, а
DbContext— «дверь» в базу данных. - 4Создай и примени миграцию
dotnet ef migrations add InitialCreateзаписывает схему в виде C#-файла, аdotnet ef database updateприменяет её к базе.
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-efМодель и DbContext
EF Core работает по соглашениям: свойство с именем Id — первичный ключ, пара ClassroomId + Classroom — внешний ключ, а List<Student> — связь «в одном классе много учеников». Ключевое слово required говорит, что Name будет столбцом NOT NULL и его обязательно нужно задать при создании объекта. Свойства DbSet<T> у DbContext соответствуют таблицам, а OnConfiguring указывает, к какой базе подключаться.
using Microsoft.EntityFrameworkCore;
public class Classroom
{
public int Id { get; set; } // primary key by convention
public required string Name { get; set; }
public List<Student> Students { get; set; } = []; // one classroom, many students
}
public class Student
{
public int Id { get; set; }
public required string Name { get; set; }
public int Score { get; set; }
public int ClassroomId { get; set; } // foreign key
public Classroom Classroom { get; set; } = null!;
}
public class SchoolContext : DbContext
{
public DbSet<Classroom> Classrooms => Set<Classroom>();
public DbSet<Student> Students => Set<Student>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlite("Data Source=school.db");
}В настоящем приложении строку подключения не пишут в OnConfiguring: контекст получает параметры через конструктор — public class SchoolContext(DbContextOptions<SchoolContext> options) : DbContext(options), — а сама строка берётся из файла конфигурации (это ты увидишь в ASP.NET Core в следующем уроке). DbContext — лёгкий короткоживущий объект: его обычно создают на один запрос или одну единицу работы, и он не потокобезопасен, поэтому делить его между параллельными задачами нельзя.
Миграции
Миграция — C#-файл, который переносит изменение модели в базу данных. migrations add сравнивает модель с её прошлым состоянием и создаёт в папке Migrations/ класс с методами Up (применить) и Down (откатить); database update выполняет ещё не применённые миграции. Добавил в класс новое свойство? Создай новую миграцию — например, AddEmail — и обнови базу. Миграции хранятся в Git, поэтому у всех в команде база с одинаковой схемой.
dotnet ef migrations add InitialCreate
dotnet ef database updateBuild started... Build succeeded. Done. To undo this action, use 'ef migrations remove' Build started... Build succeeded. Applying migration '20260927101500_InitialCreate'. Done.
LINQ to Entities: CRUD-операции
Запрос LINQ, написанный для DbSet<T>, выполняется не в памяти, а в базе: EF Core переводит дерево выражений в SQL (LINQ to Entities). Add и Remove лишь запоминают изменения, а SaveChangesAsync отправляет их в базу одной транзакцией. Объект, прочитанный из базы, отслеживается (change tracking): достаточно изменить его свойство и вызвать SaveChangesAsync — EF Core сам напишет UPDATE.
await using var db = new SchoolContext();
var tenA = new Classroom { Name = "10A" };
tenA.Students.Add(new Student { Name = "Aysel", Score = 95 });
tenA.Students.Add(new Student { Name = "Leyla", Score = 88 });
db.Classrooms.Add(tenA);
await db.SaveChangesAsync(); // INSERTs; Id values are filled in
List<string> top = await db.Students
.Where(s => s.Score >= 90)
.OrderByDescending(s => s.Score)
.Select(s => s.Name)
.ToListAsync(); // the SQL runs here
Console.WriteLine(string.Join(", ", top));
Student leyla = await db.Students.SingleAsync(s => s.Name == "Leyla");
leyla.Score = 91; // the change tracker notices this
await db.SaveChangesAsync(); // UPDATE "Students" SET "Score" = ...
db.Students.Remove(leyla);
await db.SaveChangesAsync(); // DELETE
Console.WriteLine(await db.Students.CountAsync());Aysel 1
Полезно видеть, какой SQL генерирует EF Core: ToQueryString() возвращает текст SQL запроса, не выполняя его.
var query = db.Students
.Where(s => s.Score >= 80)
.OrderByDescending(s => s.Score)
.Select(s => s.Name);
Console.WriteLine(query.ToQueryString());SELECT "s"."Name" FROM "Students" AS "s" WHERE "s"."Score" >= 80 ORDER BY "s"."Score" DESC
Связи, Include и проблема N+1
Связанные данные не загружаются автоматически: если student.Classroom или classroom.Students не запрошены в запросе, они остаются пустыми. Include(...) приносит связанные объекты в том же запросе. Для связи «многие ко многим» достаточно коллекции с обеих сторон — промежуточную таблицу EF Core создаст сам. Самая известная ошибка производительности — N+1: один запрос за списком, а потом ещё по запросу на каждый элемент.
public class Course
{
public int Id { get; set; }
public required string Title { get; set; }
public List<Student> Students { get; set; } = [];
}
// in Student: public List<Course> Courses { get; set; } = [];
// EF Core creates the join table CourseStudent automatically
var withCourses = await db.Students
.Include(s => s.Courses) // load related data in the same query
.Where(s => s.Courses.Any(c => c.Title == "Math"))
.ToListAsync();var classes = await db.Classrooms.ToListAsync();
foreach (var c in classes)
{
// one more query for every classroom: the N+1 problem
int count = await db.Students.CountAsync(s => s.ClassroomId == c.Id);
Console.WriteLine($"{c.Name}: {count}");
}var classes = await db.Classrooms
.Select(c => new { c.Name, Count = c.Students.Count })
.ToListAsync(); // a single query with a subquery
foreach (var c in classes)
Console.WriteLine($"{c.Name}: {c.Count}");var list = await db.Students
.FromSqlRaw("SELECT * FROM Students WHERE Name = '" + name + "'")
.ToListAsync(); // SQL injection!var list = await db.Students
.FromSql($"SELECT * FROM Students WHERE Name = {name}")
.ToListAsync(); // {name} becomes a SQL parameterFromSql (с EF Core 7) превращает интерполированные значения в параметры SQLГлавное
- EF Core связывает классы с таблицами:
DbContext+DbSet<T>;Id— первичный ключ,XxxId— внешний. - Миграции:
dotnet ef migrations addзаписывает схему,dotnet ef database updateприменяет её; они хранятся в Git. - LINQ над
DbSetпревращается в SQL (его показываетToQueryString()); изменения сохраняетSaveChangesAsync. - Связанные данные загружай одним запросом через
Includeили проекцию — избегай N+1. - Сырой SQL — через
FromSql($"..."); для чтения —AsNoTracking(), для массовых изменений —ExecuteUpdateAsync/ExecuteDeleteAsync.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.