Entity FrameworkMiddleTechnical
В чём разница между SaveChanges() и SaveChangesAsync()?
SaveChanges() блокирует поток до завершения записи в БД. SaveChangesAsync() освобождает поток на время I/O и поддерживает CancellationToken. В ASP.NET Core всегда используйте SaveChangesAsync() — это обязательно для масштабируемости.
SaveChanges() vs SaveChangesAsync() в EF Core
Оба метода выполняют одно и то же: отправляют накопленные изменения из ChangeTracker в базу данных в рамках транзакции. Разница — в модели выполнения.
SaveChanges()
Синхронный вызов. Блокирует текущий поток до завершения операции с БД. Использовался в EF 6 и ранних приложениях на .NET Framework.
context.Products.Add(new Product { Name = "Widget", Price = 9.99m });
int affected = context.SaveChanges();
Console.WriteLine($"Saved {affected} records");
SaveChangesAsync()
Асинхронный вызов. Освобождает поток во время ожидания ответа от БД, что критично для масштабируемости веб-приложений.
context.Products.Add(new Product { Name = "Gadget", Price = 49.99m });
int affected = await context.SaveChangesAsync();
Console.WriteLine($"Saved {affected} records");
Ключевые отличия
- Поток: SaveChanges блокирует поток; SaveChangesAsync возвращает его в пул потоков на время I/O.
- Масштабируемость: в ASP.NET Core с тысячами параллельных запросов SaveChanges может исчерпать пул потоков; SaveChangesAsync — нет.
- CancellationToken: SaveChangesAsync принимает токен отмены — можно прервать операцию при отключении клиента.
- Транзакции: поведение одинаковое — оба автоматически оборачивают изменения в транзакцию.
Перехват через SaveChangesInterceptor
EF Core 5+ позволяет перехватывать оба вызова через интерцепторы:
public class AuditInterceptor : SaveChangesInterceptor
{
public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context!;
foreach (var entry in context.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified))
{
if (entry.Entity is IAuditable auditable)
{
auditable.UpdatedAt = DateTime.UtcNow;
}
}
return await base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
Переопределение SaveChangesAsync в DbContext
public class AppDbContext : DbContext
{
public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
{
// Автоматическое заполнение аудит-полей
foreach (var entry in ChangeTracker.Entries<IAuditable>())
{
if (entry.State == EntityState.Added)
entry.Entity.CreatedAt = DateTime.UtcNow;
if (entry.State is EntityState.Added or EntityState.Modified)
entry.Entity.UpdatedAt = DateTime.UtcNow;
}
return await base.SaveChangesAsync(ct);
}
}
Подводные камни
- SaveChanges() в ASP.NET Core — антипаттерн: при пиковой нагрузке блокирует потоки и снижает throughput.
- SaveChangesAsync без CancellationToken не позволяет прервать долгую транзакцию — всегда прокидывайте токен из контроллера/endpoint.
- SaveChangesAsync не является потокобезопасным: DbContext нельзя использовать одновременно из нескольких потоков — даже с async/await.
- При переопределении SaveChangesAsync не забывайте вызывать синхронный SaveChanges отдельно или делегировать его через базовый — иначе синхронный вызов не получит аудит-логику.
- SaveChanges в консольных приложениях и фоновых сервисах вполне допустим, но SaveChangesAsync предпочтительнее для унификации кода.
- Интерцепторы, зарегистрированные только для SavingChangesAsync, не срабатывают при синхронном SaveChanges — реализуйте оба метода.
Common mistakes
- Путать SaveChanges и SaveChangesAsync в I/O-bound persistence с похожим механизмом из другой версии или платформы.
- Игнорировать runtime-границы Entity Framework: lifecycle, DI scope, SQL translation, UI thread или platform API.
- Не обсуждать null/empty/error cases и поведение под нагрузкой.
What the interviewer is testing
- Кандидат объясняет SaveChanges и SaveChangesAsync в I/O-bound persistence на конкретном примере, а не только определением.
- Указывает последствия для производительности, тестируемости и поддержки.
- Различает документированное поведение текущего стека и устаревшие практики.