Внедрение зависимостей из нескольких контекстов ядра Entity Framework для параллельного выполнения запросов

#c# #entity-framework #dependency-injection

#c# #entity-framework #внедрение зависимостей

Вопрос:

У меня есть веб-приложение, использующее Entity Framework Core 3.1, и DbContext регистрируется с использованием services.AddDbContext в начале.

 public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<MyProjectContext>(options =>
        {
            ...
        });
    }
}
  

Мне нужно запускать несколько запросов параллельно, но Entity Framework допускает только один асинхронный запрос за раз. Прочитав другие ответы здесь на SO, я думаю, мне нужно иметь несколько экземпляров MyProjectContext и запускать один запрос на экземпляр.

Чего я не знаю, так это как зарегистрировать несколько экземпляров моего DbContext и как получить их в моих контроллерах.

 public class ProductsController : ControllerBase
{
    public ProductsController(MyProjectContext context1, MyProjectContext context2, MyProjectContext context3)
    {
        var product = context1.Products.AsNoTracking().FirstAsync();
        var category = context2.Categories.AsNoTracking().FirstAsync();
        var customer = context3.Customer.AsNoTracking().FirstAsync();

        // Task.WaitAll, etc.
    }
}
  

context1 , context2 , и context3 разрешаются в одном экземпляре.

Как я могу разрешить каждый DbContext для другого экземпляра — и в то же время ограничить их для запроса?

Комментарии:

1. Эта проблема: github.com/dotnet/efcore/issues/10694 это может натолкнуть вас на некоторые мысли

Ответ №1:

Этот код будет ожидать каждого вызова перед вызовом следующего, независимо от того, один DbContext или три:

 public ProductsController(MyProjectContext context1, MyProjectContext context2, MyProjectContext context3)
{
    var product = await context1.Products.AsNoTracking().FirstAsync();
    var category = await context2.Categories.AsNoTracking().FirstAsync();
    var customer = await context3.Customer.AsNoTracking().FirstAsync();
} 
  

Чтобы запустить их параллельно, это будет выглядеть так:

 var productTask = context1.Products.AsNoTracking().FirstAsync();
var categoryTask = context2.Categories.AsNoTracking().FirstAsync();
var customerTask = context3.Customer.AsNoTracking().FirstAsync();

await Task.WhenAll(productTask, categoryTask, customerTask);
var product = productTask.Resu<
var category = categoryTask.Resu<
var customer = customerTask.Resu<
  

Однако, поскольку ваши контексты разрешаются для одного и того же экземпляра, это приведет к многопоточным исключениям доступа из DbContext.

Одним из вариантов было бы предоставить DbContextFactory для классов, где требуется больше ручного управления областью жизни DbContext.

 public ProductsController(MyProjectContextFactory contextFactory)
{
    using(var context1 = contextFactory.Create())
    {
        using(var context2 = contextFactory.Create())
        {
            using(var context3 = contextFactory.Create())
            {
                var productTask = context1.Products.AsNoTracking().FirstAsync();
                var categoryTask = context2.Categories.AsNoTracking().FirstAsync();
                var customerTask = context3.Customer.AsNoTracking().FirstAsync();

                await Task.WhenAll(productTask, categoryTask, customerTask);
                var product = productTask.Resu<
                var category = categoryTask.Resu<
                var customer = customerTask.Resu<
            }
        }
    }
}
  

Обычно я не рекомендую рассматривать подобные подходы для распараллеливания запросов, потому что важно учитывать, что каждый из этих трех объектов поступает из отдельных DbContexts, поэтому на них нельзя полагаться как на ссылки для чего-либо, что будет сохранено в любом другом DbContext. В рамках этих using блоков эти объекты будут прокси, которые отслеживаются их соответствующим DbContext, поэтому ссылка на них для обновления другого объекта (в другом DbContext) приведет к исключению. За пределами этих контекстов эти объекты необходимо будет проверить на соответствие другому DbContext, прежде чем присоединять к нему.

Другой вариант — изменить область жизненного цикла внедрения зависимостей для DbContext на экземпляр или что-то подобное, а не что-то вроде для веб-запроса. Настройка этого будет зависеть от используемой вами инфраструктуры DI. (Т.е. Autofac или Unity и т. Д.) Однако предостережение заключается в том, что все экземпляры DbContext будут отделены друг от друга, что приведет ко всевозможным проблемам, подобным описанным выше, Когда обычно таким вещам, как контроллеры и службы и т. Д., Которые могут ссылаться на DbContext, Будет предоставлен один экземпляр, отслеживающий извлеченные объекты, чтобы эти объекты могли быть связаны и т. Д. друг с другом без проблем. Если все DbContexts переключаются на предоставление для каждого экземпляра (для каждого запроса из контейнера DI), то это может привести ко всевозможным исключениям, вставке повторяющихся строк и т. Д.

С какой проблемой / проблемой вы сталкиваетесь, когда считаете, что вам нужно распараллеливать запросы? Загрузка объектов по идентификатору происходит чрезвычайно быстро, до такой степени, что, если вы не включаете много связанных объектов, обычно лучше оставить эти операции как синхронные запросы. async само по себе добавляет небольшое снижение производительности, поэтому это не является серебряной пулей производительности. Основная цель async операций — освободить поток сервера для ответа на другие запросы, пока обрабатывается особенно тяжелый запрос клиента. Это не ускоряет выполнение отдельного запроса, но в целом гарантирует, что ваш сервер будет реагировать на большее количество запросов. Распараллеливание запросов потенциально может ускорить выполнение запроса клиента, но требует значительно большей сложности для обеспечения получения соответствующего ответа и обработки объектов, отслеживаемых отдельными DbContexts. Это то, что я бы рассматривал только там, где я, безусловно, сталкиваюсь с проблемой производительности в каждом конкретном случае, а не то, что я бы использовал в качестве соглашения, чтобы попытаться превентивно решить воображаемую проблему производительности. ( async запросы добавляют небольшое снижение производительности и добавляют сложности и потенциальные проблемы в будущем, особенно при смешивании распараллеливания)

Комментарии:

1. Спасибо. Да, мой пример кода был неправильным со всеми awaits , но на самом деле я делаю WhenAll и т. Д., Как вы указали. Я создаю API, который должен запрашивать множество разных таблиц… Записи равны нулю. Все, что я делаю, это запрашиваю таблицы, сглаживающие ответ DTO для отправки обратно клиенту. Я хочу, чтобы ответ был как можно быстрее.

2. Если вы сглаживаете объектный граф связанных данных, спроецируйте его через Select или Automapper ProjectTo , используя свойства навигации, а не загружая отдельные связанные объекты. Если вы имеете дело с отдельными ссылками, все равно используйте проекцию для построения оптимальных запросов. (поскольку представлениям редко требуются все поля, и это позволит избежать возможных сценариев отложенной загрузки) async /parallel может не ускорить ваш ответ, особенно при загрузке для простых и быстрых запросов. Обязательно сравните параметры синхронизации и асинхронности / параллельности для общего времени отклика.