Перейти к основному содержимому

Entity Framework Core — первая программа

Разработчику
Загрузка симулятора первой программы…

Entity Framework Core — первая программа

EF Core — ORM для .NET

Программа на C# часто хранит данные в базе — таблицы с строками и столбцами (как Excel, только для сервера). Писать каждый раз сырой SQL вручную утомительно: легко ошибиться в синтаксисе, забыть экранировать строки, смешать логику приложения с запросами.

Entity Framework Core (EF Core) — официальная библиотека Microsoft, которая называется ORM (Object-Relational Mapping, «отображение объектов на таблицы»). Вы описываете обычные классы C# (Book, Author), а EF Core:

  • переводит запросы на языке LINQ в SQL;
  • следит, какие объекты вы изменили в памяти, и при сохранении формирует INSERT / UPDATE / DELETE;
  • ведёт миграции — версии схемы БД в коде, чтобы коллега или сервер получили те же таблицы, что и у вас.

Если ADO.NET — прямой доступ: вы сами пишете SQL и читаете строки, то EF Core ускоряет типичный CRUD (создать, прочитать, обновить, удалить). При этом полезно иногда смотреть, какой SQL реально уходит в базу — для этого ниже включим логирование.

Мы соберём консольное приложение «Книги» на SQLite — один файл books.db на диске, без установки SQL Server. Тот же DbContext потом подключим к ASP.NET Core. Обзор всех подходов: 44. Язык запросов LINQ: 29.


Словарь перед стартом

ТерминПростыми словами
База данных (БД)Хранилище таблиц; в нашем примере — файл books.db.
ТаблицаНабор строк одного типа; у нас — Books с колонками Id, Title, Year.
Сущность (entity)Класс C#, одна строка таблицы — один объект Book.
DbContext«Сессия» работы с БД: набор таблиц (DbSet), настройка подключения, вызов SaveChanges.
DbSet<T>Коллекция сущностей, привязанная к таблице (DbSet<Book> → таблица Books).
МиграцияC#-файл с инструкциями «создать таблицу / добавить колонку»; применяется командой database update.
Code FirstСначала пишем классы, EF сам предлагает схему таблиц.
CRUDCreate, Read, Update, Delete — четыре базовые операции с данными.
LINQЗапросы к коллекциям и DbSet на C# (Where, OrderBy); EF переводит их в SQL.
Отложенное выполнениеЗапрос LINQ выполняется в БД только когда вызываете ToListAsync, FirstAsync и т.п.

Что получится

ШагРезультат
МодельКласс Book → таблица Books
МиграцияФайлы в Migrations/, команда dotnet ef database update
CRUDДобавить, прочитать, изменить, удалить через DbContext
ЗапросLINQ Where / OrderBy → SQL на стороне БД

Требования

  • .NET SDK 8 или новее (dotnet --version в терминале).
  • Глобальный инструмент для миграций — отдельная утилита dotnet-ef, она читает ваш проект и генерирует файлы миграций:
dotnet tool install --global dotnet-ef
dotnet ef --version

Если версия выводится — инструмент установлен. Обновление: dotnet tool update --global dotnet-ef.


Создание проекта

dotnet new console -n BooksEf -o BooksEf
cd BooksEf
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
ПакетЗачем
EntityFrameworkCore.SqliteПровайдер: EF знает, как говорить с SQLite.
EntityFrameworkCore.DesignНужен только при разработке для команд dotnet ef migrations; в готовом exe на сервере часто достаточно только Sqlite.

Консольное приложение (console) — программа без окна браузера: весь вывод в терминал. Так проще отладить работу с БД до веба.


Модель и контекст

Класс книги

Models/Book.cs:

namespace BooksEf.Models;

public class Book
{
public int Id { get; set; }
public string Title { get; set; } = "";
public int Year { get; set; }
}

Разбор свойств:

  • Id — первичный ключ: уникальный номер строки. EF по соглашению считает свойство с именем Id или BookId ключом таблицы.
  • Title — название книги. = "" задаёт значение по умолчанию, чтобы компилятор не ругался на неинициализированную строку.
  • Year — год издания, целое число.

Один объект Book в памяти соответствует одной строке таблицы Books.

DbContext — точка входа в БД

Data/AppDbContext.cs:

using BooksEf.Models;
using Microsoft.EntityFrameworkCore;

namespace BooksEf.Data;

public class AppDbContext : DbContext
{
public DbSet<Book> Books => Set<Book>();

protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// Файл books.db появится рядом с exe после миграции
options.UseSqlite("Data Source=books.db");
}
}
СтрокаСмысл
: DbContextБазовый класс EF: подключение, отслеживание изменений, SaveChanges.
DbSet<Book> Books«Таблица книг» в коде: db.Books.Add(...), db.Books.Where(...).
Set<Book>()Фабрика набора сущностей внутри контекста.
OnConfiguringЗдесь указывают строку подключения — куда писать данные.
UseSqlite("Data Source=books.db")SQLite хранит всё в файле books.db в рабочей папке.

В реальном API строку подключения обычно берут из appsettings.json и передают через DI (внедрение зависимостей), а OnConfiguring оставляют пустым или используют только для консольных утилит — см. раздел «EF Core в ASP.NET Core».


Первая миграция

Из папки проекта (BooksEf):

dotnet ef migrations add InitialCreate
dotnet ef database update

Что происходит по шагам:

  1. migrations add InitialCreate — EF сравнивает ваши классы (Book) с текущей БД (пока пусто) и генерирует класс миграции в папке Migrations/ с методами Up (применить) и Down (откатить).
  2. database update — выполняется Up: создаётся файл books.db и таблица Books с колонками под свойства класса.

Появятся папка Migrations/ и файл books.db. Откат последней неприменённой миграции в разработке: dotnet ef migrations remove (в продакшене откаты планируют отдельно).

:::tip Что делает dotnet ef? Утилита загружает ваш проект, находит DbContext, строит модель таблиц и пишет C#-код миграции. Без пакета Microsoft.EntityFrameworkCore.Design команда dotnet ef проект не «увидит». :::


CRUD в Program.cs

Полный пример:

using BooksEf.Data;
using BooksEf.Models;

using var db = new AppDbContext();

// Create
db.Books.Add(new Book { Title = "Clean Code", Year = 2008 });
db.Books.Add(new Book { Title = "CLR via C#", Year = 2012 });
await db.SaveChangesAsync();

// Read
var recent = await db.Books
.Where(b => b.Year >= 2010)
.OrderBy(b => b.Title)
.ToListAsync();

foreach (var b in recent)
Console.WriteLine($"{b.Id}: {b.Title} ({b.Year})");

// Update
var first = await db.Books.OrderBy(b => b.Id).FirstAsync();
first.Year = 2009;
await db.SaveChangesAsync();

// Delete
var toRemove = await db.Books.FirstAsync(b => b.Title == "CLR via C#");
db.Books.Remove(toRemove);
await db.SaveChangesAsync();

Разбор по операциям

using var db = new AppDbContext();

  • Создаётся контекст — «сеанс» работы с БД.
  • using гарантирует вызов Dispose в конце: соединение освободится, даже если будет исключение.

Create — Add + SaveChangesAsync

db.Books.Add(new Book { Title = "Clean Code", Year = 2008 });
await db.SaveChangesAsync();
  • Add помечает объект как новый: в таблице строки ещё нет, Id часто присваивается после сохранения.
  • SaveChangesAsync отправляет в БД накопленные изменения одной транзакцией (всё или ничего). Для двух Add перед одним SaveChanges EF сгенерирует два INSERT.

Read — LINQ и отложенное выполнение

var recent = await db.Books
.Where(b => b.Year >= 2010)
.OrderBy(b => b.Title)
.ToListAsync();
Часть выраженияЧто делает
db.BooksОбращение к таблице как к коллекции.
.Where(b => b.Year >= 2010)Фильтр: в SQL уйдёт WHERE Year >= 2010.
.OrderBy(b => b.Title)Сортировка по названию.
.ToListAsync()Здесь запрос уходит в БД и возвращается список в память.

До вызова ToListAsync / FirstAsync / CountAsync запрос только строится — это отложенное выполнение (deferred execution). Так EF может объединить несколько условий в один SQL.

Update — изменить свойство и сохранить

var first = await db.Books.OrderBy(b => b.Id).FirstAsync();
first.Year = 2009;
await db.SaveChangesAsync();
  • FirstAsync загружает одну строку; EF отслеживает этот объект.
  • Изменение first.Year помечает сущность как изменённую; SaveChangesAsync сгенерирует UPDATE.

Delete — Remove + сохранение

db.Books.Remove(toRemove);
await db.SaveChangesAsync();
  • Remove помечает строку на удаление → SQL DELETE.

Увидеть SQL (для обучения)

Временно в OnConfiguring после UseSqlite:

options.LogTo(Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information);

Перезапустите программу — в консоли появятся сгенерированные SELECT, INSERT, UPDATE. Так проще связать LINQ из 29 с реальной БД и заметить, например, лишние запросы (проблема N+1 — в 44).


EF Core в ASP.NET Core

В веб-проекте контекст регистрируют в Program.cs — контейнер DI создаёт один DbContext на HTTP-запрос (lifetime Scoped):

builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("Default")));

// В контроллере или Minimal API:
public class BooksController(AppDbContext db) : ControllerBase
{
[HttpGet]
public async Task<List<Book>> GetAll() =>
await db.Books.AsNoTracking().ToListAsync();
}
ЭлементЗачем
Scoped DbContextОдин контекст на запрос; корректное отслеживание изменений и одна транзакция на действие.
AsNoTracking()Только чтение: EF не хранит копию объектов для UPDATE — быстрее и меньше памяти.
Строка в appsettings.jsonРазные БД для разработки и продакшена без пересборки.

Полный API с PostgreSQL: 453. Первая программа ASP.NET: 4511.


Частые ошибки

СимптомЧто проверить
dotnet ef не найденdotnet tool install --global dotnet-ef
Нет design-time factoryПакет Microsoft.EntityFrameworkCore.Design в .csproj
Пустая таблица после AddЗабыли await db.SaveChangesAsync()
«Cannot translate method X»В Where вызвали метод C#, который EF не умеет перевести в SQL — см. 291
Много запросов на один экран (N+1)Для связанных таблиц — .Include()44
books.db не там, где ждётеПуть относителен к рабочей папке при dotnet run (часто корень проекта)

Что попробовать

  1. Добавьте поле Author в Book и новую миграцию: dotnet ef migrations add AddAuthordotnet ef database update.
  2. Замените SQLite на PostgreSQL: пакет Npgsql.EntityFrameworkCore.PostgreSQL и строка подключения в конфиге.
  3. Реализуйте тот же CRUD на ADO.NET / Dapper и сравните объём кода.

Дальше


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).