tl;dr How can I use Entity Framework in a multithreaded .NET Core API application even though DbContext is not threadsafe?
Я работаю над приложением .NET Core API, предоставляющим несколько интерфейсов RESTful, которые обращаются к базе данных и считывают данные из нее, и в то же время запускаю несколько TimedHostedServices в качестве фоновых рабочих потоков, которые регулярно опрашивают данные из других веб-сервисов и сохраняют их в базе данных.
Я знаю, что DbContext не является потокобезопасным. Я прочитал много документов, сообщений в блогах и ответов здесь, в Stackoverflow, и я мог найти много (частично противоречивых) ответов на этот вопрос, но не нашел реальной «лучшей практики» при работе с DI.
Использование ServiceLifetime.Scoped по умолчанию с помощью метода расширения AddDbContext приводит к исключениям из-за условий гонки.
Я не хочу работать с блокировками (например, Semaphore), так как очевидными недостатками являются:
Не вводить MyDbContext, а DbContextOptions<MyDbContext> вместо этого, создавая контекст только тогда, когда мне нужно получить доступ к БД, используя оператор using для немедленного удаления его после чтения/записи, кажется, что много накладных расходов на использование ресурсов и излишне много открытий/закрытий соединений.
Я действительно озадачен: как это может быть достигнуто?
Я не думаю, что мой вариант использования особенный — заполнение БД из фонового рабочего и запрос к нему из уровня веб-API — поэтому должен быть осмысленный способ сделать это с ядром ef.
Большое спасибо!
Время жизни с областью действия должно быть правильным, поскольку каждый запрос выполняется в своем собственном потоке, а также запускает новую область действия. У меня никогда не было таких проблем, поэтому, пожалуйста, покажите какой-нибудь код, где у вас была такая проблема.
@Sir Rufo: но является ли время жизни с заданной областью надежным решением для фонового рабочего, когда объект DbContext жив до тех пор, пока работает приложение? Это может занять недели, месяцы до следующего развертывания или перезапуска сервера.
Я не знал о пуле соединений - уже спасибо за эту информацию! Это означает, что регулярное (не очень частое) создание новых DbContexts в фоновом рабочем процессе не будет таким уж плохим.
Ваш TimedHostedServices должен создавать прицел IOC каждый раз, когда они срабатывают, и избавляться от него, когда задача выполнена.
@PhilipDaubmeier Время жизни с ограниченной областью действия началось с получения запроса и остановилось с возвратом ответа. Забудьте о любых BackgroundWorker — приложение может остановиться в любой момент и перезапуститься при следующем запросе.
У меня были аналогичные проблемы с HostedService, где в основном DbContext жил дольше, чем я хотел (время жизни экземпляра HostedService, которое может длиться до тех пор, пока работает пул приложений). В конце концов я вместо этого внедрил DbContextFactory и просто создал и удалил контексты по мере необходимости. DbContexts очень легкие и, как уже говорили другие, не требуют дорогостоящих подключений и т. д. для каждого экземпляра.
@ESG: это похоже на решение, которое я искал - тогда глупый вопрос: как мне сказать контейнеру IOC создать новую область?
@Sir Rufo: нет, фоновый рабочий IHostedService живет, пока работает пул приложений. См.: docs.microsoft.com/en-us/aspnet/core/fundamentals/host/…
Для длительных задач вы должны создать свой DbContext в блоке Using, чтобы вы не зависели от одного соединения с базой данных в течение всего времени существования вашего приложения. Если ваш сервер базы данных перезагружается или соединение разорвано, вам нужно иметь возможность просто открыть новое соединение.
@David Browne: ответ ESG еще более элегантен — служба, размещенная по времени, порождает новую область каждый раз, когда я опрашиваю веб-службу и записываю в базу данных. DbContext внедряется в рабочую службу, которой не нужно беспокоиться об операторах using и создании DbContexts вручную. При этом контекст живет ровно столько, сколько нужно (в моем случае несколько миллисекунд)





Вы должны создавать область всякий раз, когда срабатывает ваш TimedHostedServices.
Вставьте поставщика услуг в свой конструктор:
public MyServiceService(IServiceProvider services)
{
_services = services;
}
а затем создайте область всякий раз, когда задача запускается
using (var scope = _services.CreateScope())
{
var anotherService = scope.ServiceProvider.GetRequiredService<AnotherService>();
anotherService.Something();
}
Доступен более полный пример в документе
Это была идея, которую я искал. Я переместил свой фоновый рабочий код из размещенной по времени службы в другой класс, который создается с новой областью действия каждый раз, когда срабатывает таймер (один раз каждые 5 минут). Больше никаких блокировок или бесконечных живых DbContext, никакого ручного создания DbContext, и все работает как шарм - большое спасибо!
на самом деле это не решает проблему «накладных расходов на использование ресурсов и излишнего количества открытий/закрытий соединений», не так ли? Как и в вашей области, вы должны каждый раз запрашивать DbContext.
Для этих сценариев вы можете настроить пул DbContext, но являются ли накладные расходы проблемой в конкретном проекте или нет, это совсем другой вопрос.
Другой подход к созданию собственного DbContextFactory и созданию нового экземпляра для каждого запроса.
public class DbContextFactory
{
public YourDbContext Create()
{
var options = new DbContextOptionsBuilder<YourDbContext>()
.UseSqlServer(_connectionString)
.Options;
return new YourDbContext(options);
}
}
Применение
public class Service
{
private readonly DbContextFactory _dbContextFactory;
public Service(DbContextFactory dbContextFactory)
=> _dbContextFactory = dbContextFactory;
public void Execute()
{
using (var context = _dbContextFactory.Create())
{
// use context
}
}
}
С factory вам больше не нужно беспокоиться об областях видимости, и вы избавите свой код от зависимостей ASP.NET Core.
Вы сможете выполнять запросы асинхронно, что невозможно с ограниченным DbContext без обходных путей.
Вы всегда можете быть уверены в том, какие данные сохраняются при вызове .SaveChanges(), где с ограниченным DbContext есть вероятность того, что какой-то объект был изменен в другом классе.
Спасибо за Ваш ответ! Однако я использую подход ESG, поскольку он следует принципу IoC. Также обратите внимание, что Scopes не являются зависимостью ASP.NET Core — собственный DI с областями является первоклассным гражданином, но используется во все большем количестве других. NET Core, например функции Azure и даже классические приложения.