Ответ
В ASP.NET Core при регистрации DbContext как Scoped (по умолчанию) для каждого HTTP-запроса создается один общий экземпляр контекста. Если в рамках этого запроса вы запускаете несколько асинхронных операций с БД параллельно (например, через Task.WhenAll), они будут обращаться к одному экземпляру DbContext. Это приводит к критическим проблемам, потому что DbContext не является потокобезопасным.
Основные риски:
- Состояние гонки (Race Condition): Параллельное изменение внутренних коллекций отслеживаемых сущностей (
DbSet). - Исключения: Чаще всего возникает
InvalidOperationExceptionс сообщением "A second operation was started on this context before a previous operation completed". - Порча данных: Непредсказуемые результаты при конкурентной записи.
Пример проблемного кода:
public class DataService
{
private readonly AppDbContext _context; // Один Scoped контекст на запрос
public DataService(AppDbContext context) => _context = context;
public async Task ProcessUsersAsync(List<int> userIds)
{
var tasks = userIds.Select(id => _context.Users.FindAsync(id)); // ПАРАЛЛЕЛЬНЫЙ вызов FindAsync
var users = await Task.WhenAll(tasks); // ВЫБРОСИТ ИСКЛЮЧЕНИЕ!
}
}
Правильные стратегии решения:
- Последовательное выполнение (Async/Await): Избегайте параллелизма на одном контексте. Выполняйте операции последовательно.
-
Создание новых независимых экземпляров: Для истинно параллельной работы создавайте новые экземпляры
DbContextвнутри каждой задачи. В ASP.NET Core для этого нужно использоватьIServiceScopeFactory.public class ParallelDataService { private readonly IServiceScopeFactory _scopeFactory; public ParallelDataService(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory; public async Task<List<User>> GetUsersInParallelAsync(List<int> userIds) { var tasks = userIds.Select(async id => { // Создаем новый scope и свой экземпляр контекста для каждой задачи using var scope = _scopeFactory.CreateScope(); var context = scope.ServiceProvider.GetRequiredService<AppDbContext>(); return await context.Users.FindAsync(id); }); return (await Task.WhenAll(tasks)).ToList(); } } - Использование Transient: В очень специфичных сценариях можно зарегистрировать контекст как
Transient, но это обычно антипаттерн для веб-приложений, так как теряются преимущества единой области видимости на запрос (отслеживание изменений, автоматическое управление соединением).
Вывод: Scoped DbContext предназначен для последовательной работы в рамках одного логического запроса. Для параллельных операций необходимо явно создавать изолированные экземпляры.