Час життя DbContext в ASP.NET Core: Продуктивність проти потокобезпечності в асинхронних операціях
Опануйте керування часом життя DbContext в ASP.NET Core. Дізнайтеся, коли використовувати scoped проти transient, як безпечно обробляти асинхронні операції та оптимізувати продуктивність за допомогою пулінгу DbContext.

Керування часом життя DbContext є одним із найпоширеніших джерел помилок у додатках ASP.NET Core. DbContext Entity Framework Core не є потокобезпечним, і його неправильне використання в асинхронних операціях призводить до пошкодження стану, станів гонки (race conditions) та винятків, які важко відтворити.
Один екземпляр DbContext не може використовуватися кількома потоками одночасно. Якщо асинхронний метод не очікується (await) до початку іншої операції на тому самому контексті, внутрішній стан пошкоджується. Це правило застосовується до всіх версій EF Core, включаючи EF Core 9.
Чому час життя Scoped працює для веб-запитів
Ін'єкція залежностей ASP.NET Core за замовчуванням реєструє DbContext із часом життя scoped при використанні AddDbContext<T>(). Кожен HTTP-запит отримує власний екземпляр DbContext, який видаляється після завершення запиту.
Цей підхід вирішує дві проблеми: забезпечує ізольований стан бази даних для кожного запиту та узгоджує час життя контексту з патерном Unit of Work, де зміни накопичуються під час обробки запиту та фіксуються разом наприкінці.
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));Час життя scoped підходить для стандартних потоків запит-відповідь. Дія контролера або обробник Razor Page працює в одному потоці, і поки всі асинхронні виклики правильно очікуються, DbContext залишається в узгодженому стані протягом усього запиту.
public class OrderController : ControllerBase
{
private readonly AppDbContext _context;
public OrderController(AppDbContext context)
{
_context = context;
}
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderDto dto)
{
var order = new Order { CustomerId = dto.CustomerId, Total = dto.Total };
_context.Orders.Add(order);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}
}Ключове обмеження: кожна асинхронна операція повинна завершитися до початку наступної. Наведений вище код правильний, оскільки SaveChangesAsync очікується до повернення результату методом.
Проблема потокобезпечності в паралельних операціях
Проблеми виникають, коли розробники намагаються паралелізувати операції з базою даних у межах одного запиту. Розглянемо цей помилковий приклад:
// ПОМИЛКА: Не використовувати
public async Task<IActionResult> GetDashboard()
{
var ordersTask = _context.Orders.CountAsync();
var productsTask = _context.Products.CountAsync();
var customersTask = _context.Customers.CountAsync();
// Запуск трьох запитів на одному DbContext одночасно
await Task.WhenAll(ordersTask, productsTask, customersTask);
return Ok(new { Orders = ordersTask.Result, Products = productsTask.Result, Customers = customersTask.Result });
}Цей код запускає три асинхронні запити без індивідуального очікування кожного з них. Усі три операції виконуються одночасно на одному екземплярі DbContext, порушуючи вимоги потокобезпечності EF Core. Результат непередбачуваний: іноді працює, іноді викидає InvalidOperationException, а іноді повертає неправильні дані.
Офіційна документація Microsoft чітко вказує, що асинхронні методи повинні очікуватися негайно. Внутрішній change tracker, стан з'єднання та кеш компіляції запитів не призначені для одночасного доступу.
Готовий до співбесід з .NET?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Використання IDbContextFactory для паралельних запитів
Коли паралельні операції з базою даних дійсно потрібні, IDbContextFactory<T> надає рішення. Ця фабрика створює нові екземпляри DbContext на вимогу, кожен зі своїм ізольованим станом.
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));Після реєстрації фабрики слід ін'єктувати її замість DbContext безпосередньо:
public class DashboardService
{
private readonly IDbContextFactory<AppDbContext> _contextFactory;
public DashboardService(IDbContextFactory<AppDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task<DashboardStats> GetStatsAsync()
{
// Кожне завдання отримує власний екземпляр DbContext
var ordersTask = Task.Run(async () =>
{
await using var context = _contextFactory.CreateDbContext();
return await context.Orders.CountAsync();
});
var productsTask = Task.Run(async () =>
{
await using var context = _contextFactory.CreateDbContext();
return await context.Products.CountAsync();
});
var customersTask = Task.Run(async () =>
{
await using var context = _contextFactory.CreateDbContext();
return await context.Customers.CountAsync();
});
await Task.WhenAll(ordersTask, productsTask, customersTask);
return new DashboardStats
{
Orders = ordersTask.Result,
Products = productsTask.Result,
Customers = customersTask.Result
};
}
}Кожне завдання створює, використовує та видаляє власний контекст. Оператор await using гарантує правильне очищення навіть у випадку виникнення винятку.
Пулінг DbContext для високопродуктивних додатків
Створення нового DbContext передбачає виділення пам'яті, ініціалізацію change tracker та налаштування внутрішніх кешів. Для високопродуктивних додатків, що обробляють тисячі запитів на секунду, ці накладні витрати стають вимірюваними.
Пулінг DbContext вирішує цю проблему шляхом повторного використання екземплярів контексту. Коли пулований контекст видаляється, EF Core скидає його стан і повертає до пулу замість того, щоб передавати на збирання сміття.
builder.Services.AddDbContextPool<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")),
poolSize: 128);Бенчмарки Dave Callan показують, що пулінг зменшує час створення контексту більш ніж на 90% у мікробенчмарках. Однак реальний вплив залежить від патернів запитів додатку. Якщо запити до бази даних домінують у часі обробки запиту, накладні витрати на створення контексту порівняно незначні.
Пулінг вводить одне обмеження: стан DbContext скидається при поверненні до пулу. Будь-які користувацькі поля або властивості, додані до класу DbContext, втратять свої значення. Change tracker очищується, тому незафіксовані зміни зникають.
Поєднання пулінгу з IDbContextFactory
Для додатків, яким потрібна як продуктивність пулінгу, так і підтримка паралельних запитів, слід використовувати AddPooledDbContextFactory:
builder.Services.AddPooledDbContextFactory<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")),
poolSize: 128);Ця реєстрація надає IDbContextFactory<AppDbContext>, де контексти беруться з пулу. Кожен виклик CreateDbContext() отримує екземпляр з пулу, а його видалення повертає екземпляр до пулу.
public class BatchProcessor
{
private readonly IDbContextFactory<AppDbContext> _contextFactory;
public BatchProcessor(IDbContextFactory<AppDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task ProcessBatchAsync(IEnumerable<OrderUpdate> updates)
{
// Паралельна обробка частинами з пулованими контекстами
var chunks = updates.Chunk(100);
var tasks = chunks.Select(async chunk =>
{
await using var context = _contextFactory.CreateDbContext();
foreach (var update in chunk)
{
var order = await context.Orders.FindAsync(update.OrderId);
if (order != null)
{
order.Status = update.NewStatus;
}
}
await context.SaveChangesAsync();
});
await Task.WhenAll(tasks);
}
}Аналіз Milan Jovanovic надає додаткові бенчмарки, що показують продуктивність пулованої фабрики в сценаріях пакетної обробки.
Blazor Server: Особливий випадок
Додатки Blazor Server вимагають особливої уваги при керуванні часом життя DbContext. Схема Blazor (circuit) зберігається протягом кількох взаємодій користувача, на відміну від HTTP-запитів, які мають чіткі межі.
За замовчуванням час життя scoped стає проблематичним: один екземпляр DbContext живе протягом усього часу існування схеми, потенційно годинами. Довгоживучі контексти накопичують відстежувані сутності, збільшуючи використання пам'яті та сповільнюючи операції.
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));У компонентах Blazor слід ін'єктувати фабрику та створювати короткоживучі контексти:
public partial class OrderList : ComponentBase
{
[Inject]
private IDbContextFactory<AppDbContext> ContextFactory { get; set; } = default!;
private List<Order> _orders = new();
protected override async Task OnInitializedAsync()
{
await using var context = ContextFactory.CreateDbContext();
_orders = await context.Orders
.AsNoTracking()
.OrderByDescending(o => o.CreatedAt)
.Take(50)
.ToListAsync();
}
}Виклик AsNoTracking() особливо важливий у Blazor Server. Без нього кожна завантажена сутність залишається в change tracker, споживаючи пам'ять до завершення схеми.
Фонові служби та Hosted Services
Фонові служби, зареєстровані як singleton, не можуть ін'єктувати scoped DbContext безпосередньо. Служба переживає будь-яку область видимості, створюючи помилки невідповідності часу життя.
public class OrderProcessingService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public OrderProcessingService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var pendingOrders = await context.Orders
.Where(o => o.Status == OrderStatus.Pending)
.Take(10)
.ToListAsync(stoppingToken);
foreach (var order in pendingOrders)
{
order.Status = OrderStatus.Processing;
}
await context.SaveChangesAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}IServiceScopeFactory створює нову область видимості для кожної ітерації. Область видимості та DbContext всередині неї видаляються наприкінці кожної ітерації циклу.
Альтернативно можна використовувати IDbContextFactory для більш детального контролю:
public class OrderProcessingService : BackgroundService
{
private readonly IDbContextFactory<AppDbContext> _contextFactory;
public OrderProcessingService(IDbContextFactory<AppDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await using var context = _contextFactory.CreateDbContext();
var pendingOrders = await context.Orders
.Where(o => o.Status == OrderStatus.Pending)
.Take(10)
.ToListAsync(stoppingToken);
foreach (var order in pendingOrders)
{
order.Status = OrderStatus.Processing;
}
await context.SaveChangesAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}Типові помилки, що спричиняють порушення потокобезпечності
Кілька патернів постійно викликають проблеми з потокобезпечністю DbContext у продакшн-коді:
Зберігання DbContext у статичних полях або singleton: DbContext ніколи не повинен переживати свою призначену область видимості. Статичні посилання роблять той самий екземпляр доступним з кількох потоків.
Асинхронні виклики типу fire-and-forget: Запуск асинхронної операції без очікування на неї при продовженні використання контексту в поточному потоці створює одночасний доступ.
// ПОМИЛКА: Не використовувати
public void UpdateAndNotify(int orderId)
{
var order = _context.Orders.Find(orderId);
order.Status = OrderStatus.Shipped;
_context.SaveChangesAsync(); // Не очікується!
_notificationService.SendAsync(order.CustomerId); // Контекст може все ще зберігати
}Ін'єкція DbContext у singleton-служби: DI-контейнер викидає виняток у середовищі розробки, але деякі конфігурації маскують цю помилку.
Використання DbContext через кілька await без розуміння потоку виконання: Кожен await є точкою призупинення. Якщо відновлений код виконується в іншому потоці, ніж очікувалося, може виникнути одночасний доступ з іншими шляхами коду.
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Посібник з прийняття рішень щодо конфігурації часу життя DbContext
Вибір правильної конфігурації DbContext залежить від типу додатку та вимог до продуктивності:
- Стандартний web API або MVC-додаток: Використовуйте
AddDbContext<T>()з часом життя scoped за замовчуванням. Це правильно охоплює більшість сценаріїв. - Високопродуктивний API (тисячі запитів/секунду): Використовуйте
AddDbContextPool<T>()для зменшення накладних витрат на виділення пам'яті. - Додаток, що вимагає паралельних запитів до бази даних: Використовуйте
AddDbContextFactory<T>()абоAddPooledDbContextFactory<T>(). - Додаток Blazor Server: Використовуйте
AddDbContextFactory<T>()з короткоживучими контекстами, що створюються для кожної операції. - Фонові служби: Використовуйте
IServiceScopeFactoryабоIDbContextFactory<T>()для створення контекстів всередині служби. - Пакетна обробка з паралелізмом: Використовуйте
AddPooledDbContextFactory<T>()для оптимальної пропускної здатності.
Для глибшого розгляду асинхронних патернів в ASP.NET Core модуль асинхронного програмування охоплює пов'язані концепції. Модуль поглибленого вивчення EF Core розширює оптимізацію запитів та поведінку change tracker, що обговорюються тут.
Чи знайдеш ти помилку в .NET?
Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Автор:
Anthony Fillion-MailletЗасновник SharpSkill
Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.
Оновлено 7 вересня 2026 р.
Теги
Поділитися
Пов'язані статті

.NET 9 Blazor: Full-Stack розробка з Blazor United у 2026 році
.NET 9 Blazor United об'єднує статичний SSR, Server та WebAssembly режими рендерингу в єдиний full-stack фреймворк. Практичний посібник з режимами рендерингу, streaming rendering, dependency injection та production-ready патернами.

Entity Framework Core: оптимізація продуктивності та найкращі практики у 2026 році
Оптимізація продуктивності EF Core 10 з AsNoTracking, скомпільованими запитами, split queries, пакетними операціями та новим оператором LeftJoin. Практичні приклади C# для продакшн-застосунків .NET 10.

SignalR в ASP.NET Core 2026: Комунікація в Реальному Часі, Хаби та Питання на Співбесідах
Повний посібник з SignalR в ASP.NET Core. Дізнайтеся як реалізувати комунікацію в реальному часі, налаштувати хаби, керувати групами та підготуватися до технічних співбесід.