.NET Tutorial
Junior6 мин чтенияВопрос 1 из 14

Что такое ORM и EF Core: DbContext, DbSet, первые запросы и миграции?

Идея ORM

База данных

customers

orders

C#

class Customer
Id, Name, Orders

class Order
Id, Total, CustomerId

EF Core
маппинг + генерация SQL
+ отслеживание изменений

База данных

customers

orders

C#

class Customer
Id, Name, Orders

class Order
Id, Total, CustomerId

EF Core
маппинг + генерация SQL
+ отслеживание изменений

Объекты в коде ↔ строки в таблицах

Без ORM приходится писать SQL вручную, открывать соединение, читать DataReader и переносить поля в объекты. ORM берёт эту рутину на себя.

Модель и DbContext

Сущности
public class Customer
{
    public int Id { get; set; }                          // по соглашению Id — первичный ключ
    public required string Name { get; set; }
    public required string Email { get; set; }
    public List<Order> Orders { get; set; } = [];        // навигационное свойство: один-ко-многим
}

public class Order
{
    public int Id { get; set; }
    public decimal Total { get; set; }
    public DateTime CreatedAt { get; set; }
    public int CustomerId { get; set; }                  // внешний ключ
    public Customer Customer { get; set; } = null!;
}
DbContext
public class ShopDbContext(DbContextOptions<ShopDbContext> options) : DbContext(options)
{
    public DbSet<Customer> Customers => Set<Customer>();
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnModelCreating(ModelBuilder b)
    {
        b.Entity<Customer>().HasIndex(c => c.Email).IsUnique();
        b.Entity<Order>().Property(o => o.Total).HasPrecision(12, 2);
    }
}
Регистрация в ASP.NET Core
builder.Services.AddDbContext<ShopDbContext>(o =>
    o.UseNpgsql(builder.Configuration.GetConnectionString("Shop")));   // DbContext — Scoped: один на HTTP-запрос

Чтение данных

C#
// SELECT ... FROM orders WHERE total > @p ORDER BY created_at DESC LIMIT 10
var bigOrders = await db.Orders
    .Where(o => o.Total > 1_000_000m)
    .OrderByDescending(o => o.CreatedAt)
    .Take(10)
    .ToListAsync(ct);

var customer = await db.Customers.FirstOrDefaultAsync(c => c.Email == email, ct);

// Связанные данные одним запросом (JOIN)
var withOrders = await db.Customers
    .Include(c => c.Orders)
    .FirstOrDefaultAsync(c => c.Id == id, ct);

// Только нужные поля — проекция в DTO, без отслеживания
var list = await db.Orders
    .Select(o => new OrderListItem(o.Id, o.Customer.Name, o.Total))
    .ToListAsync(ct);
PostgreSQLEF CoreВаш кодIQueryable — покатолько деревовыраженийdb.Orders.Where(...).OrderBy(..-.).Take(10)ToListAsync()перевод выражения вSQLSELECT ... WHERE total >@p ORDER BY ... LIMIT 10строкиList of Order (объекты)
PostgreSQLEF CoreВаш кодIQueryable — покатолько деревовыраженийdb.Orders.Where(...).OrderBy(..-.).Take(10)ToListAsync()перевод выражения вSQLSELECT ... WHERE total >@p ORDER BY ... LIMIT 10строкиList of Order (объекты)
Как LINQ превращается в SQL

Запись данных

C#
// INSERT
var customer = new Customer { Name = "Анна", Email = "anna@mail.uz" };
db.Customers.Add(customer);
await db.SaveChangesAsync(ct);          // после сохранения customer.Id заполнен

// UPDATE — достаточно изменить загруженный объект
var order = await db.Orders.FindAsync([orderId], ct);
order!.Total = 950_000m;
await db.SaveChangesAsync(ct);          // EF сам увидит изменение и сгенерирует UPDATE

// DELETE
db.Orders.Remove(order);
await db.SaveChangesAsync(ct);

// Массовые операции без загрузки объектов (EF Core 7+)
await db.Orders.Where(o => o.CreatedAt < cutoff).ExecuteDeleteAsync(ct);

DbContext отслеживает загруженные объекты (Change Tracker): при SaveChanges он сравнивает текущие значения с исходными и формирует нужные команды — все в одной транзакции.

Миграции

Shell
dotnet tool install --global dotnet-ef
dotnet ef migrations add InitialCreate          # C#-файл с описанием изменений схемы
dotnet ef database update                       # применить к БД
dotnet ef migrations add AddCustomerPhone       # после изменения модели
dotnet ef migrations script --idempotent -o migrate.sql   # SQL-скрипт для продакшена

Изменили класс
(добавили Phone)

migrations add
AddCustomerPhone

Ревью сгенерированной
миграции

database update
или SQL-скрипт в CI/CD

Схема БД
+ таблица __EFMigrationsHistory

Изменили класс
(добавили Phone)

migrations add
AddCustomerPhone

Ревью сгенерированной
миграции

database update
или SQL-скрипт в CI/CD

Схема БД
+ таблица __EFMigrationsHistory

Цикл работы с миграциями

EF Core, Dapper или ADO.NET

EF Core Dapper ADO.NET
SQL пишет EF Core (из LINQ) Разработчик Разработчик
Маппинг в объекты Автоматически Автоматически Вручную
Отслеживание изменений Да Нет Нет
Миграции схемы Да Нет Нет
Скорость разработки Высокая Средняя Низкая
Контроль над SQL Средний Полный Полный
Когда Большинство CRUD и бизнес-логики Сложные отчёты, критичные по скорости запросы Особые случаи, низкоуровневый код

Часто их комбинируют: EF Core для основной логики, Dapper или FromSql — для тяжёлых отчётов.

Типичные ошибки новичков

Вопросы на засыпку

Почему DbContext регистрируют как Scoped?

DbContext — лёгкая короткоживущая «единица работы»: он накапливает отслеживаемые объекты и не потокобезопасен. Один контекст на HTTP-запрос изолирует запросы друг от друга и освобождает память после ответа. Singleton-контекст разрастался бы и ломался при параллельных запросах.

Что такое навигационное свойство?

Свойство сущности, указывающее на связанную сущность или коллекцию (Order.Customer, Customer.Orders). По ним EF Core строит JOIN-ы в запросах (Include, обращения в Select) и понимает связи при сохранении.

Где выполнится фильтр: в базе или в памяти?

Если фильтр применён к IQueryable (до ToList/AsEnumerable), EF Core переведёт его в SQL — выполнится в базе. После материализации (ToList()) дальнейшие операции LINQ выполняются в памяти над уже загруженными объектами.

Зачем миграции, если можно менять таблицы вручную?

Миграции версионируют схему вместе с кодом: каждое изменение воспроизводимо, проходит ревью, одинаково применяется на машинах разработчиков, в тестах и продакшене. Ручные изменения приводят к расхождению схем между окружениями.

Итог

ORM связывает объекты C# и таблицы БД; в .NET это прежде всего EF Core: сущности описывают таблицы, DbContext — сессию работы, DbSet — точку входа для LINQ-запросов, которые переводятся в SQL. Изменения сохраняются через SaveChangesAsync, схема развивается миграциями. Используйте проекции и DTO, не загружайте лишнего и помните, какой SQL генерируется. Подробности — в следующих вопросах раздела.