EF core DbContext в многопоточном приложении API

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), так как очевидными недостатками являются:

  • код загрязнен блокировками и try/catch/finally для безопасного снятия блокировок
  • на самом деле это не кажется «надежным», то есть когда я забываю заблокировать область, которая обращается к DbContext.
  • кажется избыточным и «неестественным» искусственно синхронизировать доступ к базе данных в приложении при работе с базой данных, которая также обрабатывает одновременные подключения и доступ

Не вводить MyDbContext, а DbContextOptions<MyDbContext> вместо этого, создавая контекст только тогда, когда мне нужно получить доступ к БД, используя оператор using для немедленного удаления его после чтения/записи, кажется, что много накладных расходов на использование ресурсов и излишне много открытий/закрытий соединений.

Вопрос

Я действительно озадачен: как это может быть достигнуто?

Я не думаю, что мой вариант использования особенный — заполнение БД из фонового рабочего и запрос к нему из уровня веб-API — поэтому должен быть осмысленный способ сделать это с ядром ef.

Большое спасибо!

кажется, что много накладных расходов на использование ресурсов и излишне много открытий/закрытий соединений - нет, это не из-за пула соединений
Sir Rufo 20.04.2019 22:46

Время жизни с областью действия должно быть правильным, поскольку каждый запрос выполняется в своем собственном потоке, а также запускает новую область действия. У меня никогда не было таких проблем, поэтому, пожалуйста, покажите какой-нибудь код, где у вас была такая проблема.

Sir Rufo 20.04.2019 22:50

@Sir Rufo: но является ли время жизни с заданной областью надежным решением для фонового рабочего, когда объект DbContext жив до тех пор, пока работает приложение? Это может занять недели, месяцы до следующего развертывания или перезапуска сервера.

Philip Daubmeier 20.04.2019 23:00

Я не знал о пуле соединений - уже спасибо за эту информацию! Это означает, что регулярное (не очень частое) создание новых DbContexts в фоновом рабочем процессе не будет таким уж плохим.

Philip Daubmeier 20.04.2019 23:03

Ваш TimedHostedServices должен создавать прицел IOC каждый раз, когда они срабатывают, и избавляться от него, когда задача выполнена.

ESG 20.04.2019 23:54

@PhilipDaubmeier Время жизни с ограниченной областью действия началось с получения запроса и остановилось с возвратом ответа. Забудьте о любых BackgroundWorker — приложение может остановиться в любой момент и перезапуститься при следующем запросе.

Sir Rufo 21.04.2019 00:09

У меня были аналогичные проблемы с HostedService, где в основном DbContext жил дольше, чем я хотел (время жизни экземпляра HostedService, которое может длиться до тех пор, пока работает пул приложений). В конце концов я вместо этого внедрил DbContextFactory и просто создал и удалил контексты по мере необходимости. DbContexts очень легкие и, как уже говорили другие, не требуют дорогостоящих подключений и т. д. для каждого экземпляра.

sellotape 21.04.2019 11:33

@ESG: это похоже на решение, которое я искал - тогда глупый вопрос: как мне сказать контейнеру IOC создать новую область?

Philip Daubmeier 21.04.2019 12:15

@Sir Rufo: нет, фоновый рабочий IHostedService живет, пока работает пул приложений. См.: docs.microsoft.com/en-us/aspnet/core/fundamentals/host/…

Philip Daubmeier 21.04.2019 12:18

Для длительных задач вы должны создать свой DbContext в блоке Using, чтобы вы не зависели от одного соединения с базой данных в течение всего времени существования вашего приложения. Если ваш сервер базы данных перезагружается или соединение разорвано, вам нужно иметь возможность просто открыть новое соединение.

David Browne - Microsoft 21.04.2019 18:33

@David Browne: ответ ESG еще более элегантен — служба, размещенная по времени, порождает новую область каждый раз, когда я опрашиваю веб-службу и записываю в базу данных. DbContext внедряется в рабочую службу, которой не нужно беспокоиться об операторах using и создании DbContexts вручную. При этом контекст живет ровно столько, сколько нужно (в моем случае несколько миллисекунд)

Philip Daubmeier 22.04.2019 08:14
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
11
11
11 770
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вы должны создавать область всякий раз, когда срабатывает ваш TimedHostedServices.

Вставьте поставщика услуг в свой конструктор:

public MyServiceService(IServiceProvider services)
{
    _services = services;
}

а затем создайте область всякий раз, когда задача запускается

using (var scope = _services.CreateScope())
{
    var anotherService = scope.ServiceProvider.GetRequiredService<AnotherService>();

    anotherService.Something();
}

Доступен более полный пример в документе

Это была идея, которую я искал. Я переместил свой фоновый рабочий код из размещенной по времени службы в другой класс, который создается с новой областью действия каждый раз, когда срабатывает таймер (один раз каждые 5 минут). Больше никаких блокировок или бесконечных живых DbContext, никакого ручного создания DbContext, и все работает как шарм - большое спасибо!

Philip Daubmeier 21.04.2019 22:44

на самом деле это не решает проблему «накладных расходов на использование ресурсов и излишнего количества открытий/закрытий соединений», не так ли? Как и в вашей области, вы должны каждый раз запрашивать DbContext.

Daniel Botero Correa 13.09.2019 16:00

Для этих сценариев вы можете настроить пул DbContext, но являются ли накладные расходы проблемой в конкретном проекте или нет, это совсем другой вопрос.

ESG 13.09.2019 21:34

Другой подход к созданию собственного 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 и даже классические приложения.

Philip Daubmeier 22.04.2019 08:06

Другие вопросы по теме