Параллельные запросы EF Core с внедрением DbContext в ASP.NET Core - PullRequest
0 голосов
/ 17 октября 2018

Я пишу веб-приложение ASP.NET Core, которому требуются все данные из некоторых таблиц моей базы данных, чтобы впоследствии упорядочить их в читаемый формат для некоторого анализа.

Моя проблема заключается в том, что эти данные потенциально массивныи поэтому, чтобы повысить производительность, я решил получать эти данные параллельно, а не по одной таблице за раз.

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

Приведенный ниже код создает это исключение:

---> (Inner Exception #6) System.ObjectDisposedException: Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
Object name: 'MyDbContext'.
   at Microsoft.EntityFrameworkCore.DbContext.CheckDisposed()
   at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
   at Microsoft.EntityFrameworkCore.DbContext.get_ChangeTracker()

Основной проект ASP.NET:

Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    services.AddDistributedMemoryCache();

    services.AddDbContext<AmsdbaContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("ConnectionString"))
            .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));

    services.AddSession(options =>
    {
        options.Cookie.HttpOnly = true;
    });
}

public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
    if (HostingEnvironment.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    loggerFactory.AddLog4Net();
    app.UseStaticFiles();
    app.UseCookiePolicy();
    app.UseSession();
    app.UseMvc();
}

Метод действия контроллера:

[HttpPost("[controller]/[action]")]
public ActionResult GenerateAllData()
{
    List<CardData> cardsData;

    using (var scope = _serviceScopeFactory.CreateScope())
    using (var dataFetcher = new DataFetcher(scope))
    {
        cardsData = dataFetcher.GetAllData(); // Calling the method that invokes the method 'InitializeData' from below code
    }

    return something...;
}

.NET Core Library проект:

InitializeData DataFetcher - чтобы получить все записи таблицы в соответствии с некоторыми нерелевантными параметрами:

private void InitializeData()
{
    var tbl1task = GetTbl1FromDatabaseTask();
    var tbl2task = GetTbl2FromDatabaseTask();
    var tbl3task = GetTbl3FromDatabaseTask();

    var tasks = new List<Task>
    {
        tbl1task,
        tbl2task,
        tbl3task,
    };

    Task.WaitAll(tasks.ToArray());

    Tbl1 = tbl1task.Result;
    Tbl2 = tbl2task.Result;
    Tbl3 = tbl3task.Result;
}

Пример задачи DataFetcher:

private async Task<List<SomeData>> GetTbl1FromDatabaseTask()
{
    using (var amsdbaContext = _serviceScope.ServiceProvider.GetRequiredService<AmsdbaContext>())
    {
        amsdbaContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
        return await amsdbaContext.StagingRule.Where(x => x.SectionId == _sectionId).ToListAsync();
    }
}

Ответы [ 2 ]

0 голосов
/ 17 октября 2018

Я не уверен, что вам здесь действительно нужно несколько контекстов.Вы заметили, что в документах EF Core есть это заметное предупреждение:

Предупреждение

EF Core не поддерживает несколько параллельных операций, выполняемых натот же экземпляр контекста.Вы должны всегда ждать завершения операции, прежде чем начинать следующую операцию.Обычно это делается с помощью ключевого слова await в каждой асинхронной операции.

Это не совсем точно, или, скорее, это просто вводит в заблуждение.На самом деле вы можете запускать параллельные запросы для одного экземпляра контекста.Проблема заключается в отслеживании изменений EF и исправлении объектов.Эти типы вещей не поддерживают одновременное выполнение нескольких операций, так как им нужно иметь стабильное состояние для работы при выполнении своей работы.Однако это действительно ограничивает вашу способность делать определенные вещи.Например, если вы выполняете параллельные запросы сохранения / выбора, результаты могут быть искажены.Возможно, вы не вернете вещи, которые действительно есть сейчас, или отслеживание изменений может быть испорчено во время попытки создания необходимых операторов вставки / обновления и т. Д. Однако, если вы выполняете неатомарные запросы, такие как выборки в независимых таблицахКак вы и хотели, здесь нет реальной проблемы, особенно если вы не планируете выполнять дальнейшие операции, такие как редактирование выбранных объектов, и просто планируете вернуть их в представление или что-то в этом роде.

Если вы действительно решаете, что вам нужны отдельные контексты, ваша лучшая ставка - это новый контекст с использованием.Я на самом деле не пробовал этого раньше, но вы должны быть в состоянии внедрить DbContextOptions<AmsdbaContext> в ваш класс, где происходят эти операции.Он уже должен быть зарегистрирован в коллекции служб, поскольку он внедряется в ваш контекст, когда коллекция служб создает его.Если нет, вы всегда можете просто создать новый:

var options = new DbContextOptionsBuilder()
    .UseSqlServer(connectionString)
    .Build()
    .Options;

В любом случае, тогда:

List<Tbl1> tbl1data;
List<Tbl2> tbl2data;
List<Tbl3> tbl3data;

using (var tbl1Context = new AmsdbaContext(options))
using (var tbl2Context = new AmsdbaContext(options))
using (var tbl3Context = new AmsdbaContext(options))
{
    var tbl1task = tbl1Context.Tbl1.ToListAsync();
    var tbl2task = tbl2Context.Tbl2.ToListAsync();
    var tbl3task = tbl3Context.Tbl3.ToListAsync();

    tbl1data = await tbl1task;
    tbl2data = await tbl2task;
    tbl3data = await tbl3task;
}

Лучше использовать await для получения фактического результата.Таким образом, вам даже не нужно WaitAll / WhenAll / etc.и вы не блокируете вызов Result.Поскольку задачи возвращаются горячими или уже запущены, достаточно просто отложить вызовы до тех пор, пока они не будут созданы, достаточно, чтобы купить параллельную обработку.

Просто будьте осторожны с этим, чтобы выбрать все, что вам нужно в течение использования.Теперь, когда EF Core поддерживает отложенную загрузку, если вы используете это, попытка получить доступ к ссылке или свойству коллекции, которое не было загружено, вызовет ObjectDisposedException, так как контекст исчезнет.

0 голосов
/ 17 октября 2018

Простой ответ - нет.Вам нужен альтернативный способ создания экземпляров dbcontext.Стандартный подход заключается в получении одного и того же экземпляра для всех запросов на DbContext в одном и том же запросе HttpRequest.Вы можете переопределить ServiceLifetime, но это затем изменит поведение ВСЕХ запросов.

  • Вы можете зарегистрировать второй DbContext (подкласс, интерфейс) с другим временем жизни службы.Даже тогда вам нужно обрабатывать создание вручную, так как вам нужно вызывать его один раз для каждого потока.

  • Вы создаете их вручную.

СтандартDI здесь просто заканчивается.Этого ОЧЕНЬ не хватает, даже по сравнению со старыми средами MS DI, где вы могли бы создать отдельный класс обработки с атрибутом для переопределения создания.

...