Ответ
В 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 предназначен для последовательной работы в рамках одного логического запроса. Для параллельных операций необходимо явно создавать изолированные экземпляры.
Ответ 18+ 🔞
А, блядь, вот это тема! Смотри, сейчас объясню на пальцах, как эта хуйня работает, а то многие влетают, как слепые котята.
В общем, представляешь: у тебя в ASP.NET Core есть этот самый DbContext, который по умолчанию зарегистрирован как Scoped. Это значит, что на каждый пришедший HTTP-запрос создаётся один, блядь, единственный экземпляр контекста на весь запрос. И все твои сервисы в рамках этого запроса делят этого одного несчастного контекста, как последнюю палку колбасы.
И вроде бы всё норм, пока ты не начинаешь выёбываться и пытаться в параллельные запросы к базе из одного контекста. Типа, "ой, а давайте я тут Task.WhenAll запущу, чтобы быстрее было!". И тут начинается пиздец, потому что DbContext — он, сука, не потокобезопасный. Вообще. Ни капли.
Что происходит, если начать его насиловать параллельными вызовами? Да хуйня полная:
- Состояние гонки (Race Condition): Внутри контекста есть коллекции сущностей, которые он отслеживает. Представь, что два потока одновременно пытаются туда что-то пихнуть или изменить. Это как два мужика в одной сортирной кабинке — места на всех не хватит, и всё кончится скандалом и говном на полу.
- Исключения: Чаще всего тебе прилетит
InvalidOperationExceptionс текстом вроде "A second operation was started on this context before a previous operation completed". Это он тебе вежливо намекает: "Мудила, дождись, пока первый запрос дохуярится, а потом уже второй запускай!" - Порча данных: Самый страшный сценарий. Можешь получить в базу такую ахинею, что потом будешь неделю откатывать и плакать.
Вот, смотри, как НЕ НАДО делать. Типичный пример долбоёба, который ещё не обжёгся:
public class DataService
{
private readonly AppDbContext _context; // Один контекст на всех
public DataService(AppDbContext context) => _context = context;
public async Task ProcessUsersAsync(List<int> userIds)
{
// Опа-на! Запускаем кучу FindAsync параллельно на одном контексте!
var tasks = userIds.Select(id => _context.Users.FindAsync(id));
var users = await Task.WhenAll(tasks); // ЖДИ ИСКЛЮЧЕНИЯ, ДУРИЛА!
}
}
Этот код упадёт с треском, и будет прав. Потому что нельзя.
Так как же делать-то, если овердохуища данных и хочется параллельно?
Вариант первый, самый простой и правильный в 90% случаев: не выёбывайся, делай последовательно. Просто 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();
// Из этого scope достаём СВОЙ, личный, ни с кем не делимый экземпляр контекста.
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// И вот теперь в этой задаче можно спокойно работать. Она не знает о других задачах.
return await context.Users.FindAsync(id);
});
// Теперь Task.WhenAll не сломает всё к хуям, потому что у каждой задачи свой контекст.
return (await Task.WhenAll(tasks)).ToList();
}
}
Видишь разницу? Каждая задача живёт в своей песочнице. Одна упала — остальным похуй. Одна долго работает — остальные не ждут. Красота!
Кто-то может сказать: "А давайте зарегистрируем контекст как Transient, и не надо будет с этими scope'ами ебаться!" Ну, типа, чтобы каждый раз новый инстанс создавался. Так-то оно так, но это обычно антипаттерн для веб-приложений. Потому что теряется вся магия работы с контекстом в рамках одного запроса: отслеживание изменений сущностей, автоматическое открытие и закрытие соединения с базой. Получишь другие грабли по лицу.
Короче, вывод, блядь, простой:
Scoped DbContext — он для последовательной работы в рамках одного запроса. Это как рабочий инструмент: одна отвертка на бригаду — можно передавать по очереди, но пытаться закручивать два винта одновременно одной отвёрткой — только себе навредишь. Хочешь реального параллелизма — будь добр, создавай каждому потоку (задаче) свой собственный, новый инструмент, через IServiceScopeFactory. И тогда всё будет пучком.