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 на конкретном примере, а не только определением.
  • Указывает последствия для производительности, тестируемости и поддержки.
  • Различает документированное поведение текущего стека и устаревшие практики.

Sources

Related topics