dbContext is Disposed in a Time Hosted Service

huangapple go评论52阅读模式
英文:

dbContext is Disposed in a Time Hosted Service

问题

I understand you want a translation of the code-related content. Here's the translated code section:

我正在尝试让我的 TimeHostedService  DBContext 协同工作,但我总是遇到错误:“无法访问已处置的上下文实例”。所以这是我的代码:

首先,我有我的 `DoWork` 方法在我的 Time Hosted Service 中。因为我调用的类(`IEasyConfigService`)是作用域的,而 TimeHosted Service 是单例的,所以我必须使用 scope.ServiceProvider 来实现它。我认为,到目前为止这是正确的。
```csharp
private void DoWork(object? state)
{
    using var scope = _serviceScopeFactory.CreateScope();
    var easyConfigService = scope.ServiceProvider.GetRequiredService<IEasyConfigService>();
    easyConfigService.ExecuteUnfinishedEasyConfigs();
}

现在,我的 ExecuteUnfinishedEasyConfigs 函数正在收集来自数据库的所有未完成的 Gateway。然后将其传递给方法 'TryExecuteEasyConfig'。

public async Task ExecuteUnfinishedEasyConfigs()
{
    var gateways = _context.Gateways.Include(g => g.Machine)
        .Where(g => g.EasyConfigStatus == EasyConfigStatus.OnGoing)
        .Include(g => g.Machine)
        .ThenInclude(m => m.MachineType)
        .ToList();
    await Parallel.ForEachAsync(gateways, async (gateway, _) => await TryExecuteEasyConfig(gateway));
}

这一部分可以正常工作,因为 _context 在该类的构造函数中通过 DI 进行了设置。

public class EasyConfigService : IEasyConfigService {
    private readonly SuncarContext _context;
    
    public EasyConfigService(SuncarContext context)
    {
        _context = context;
    }
}

但是在 TryExecuteEasyConfig 方法内部,该方法也在类 EasyConfigService 中,如果我尝试调用 _context.SaveChangesAsync(),我会收到上述的已处置错误。

private async Task TryExecuteEasyConfig(Gateway gateway)
{
    // 进行一些操作
    await _context.SaveChangesAsync();
}

我不知道为什么我可以先从 DBContext 中获取一些信息,然后 SaveChangesAsync() 失败了。为什么 _context 现在已被处置?

起初我认为是因为我使用了 Parallel.ForEachAsync,但即使我使用普通的 foreach 也会发生这种情况。是否有一种安全地在 TimeHostedService 内部使用 DBContext 的方法?

编辑 2023年6月15日 11:12

普通的 foreach 如下所示:

foreach (var gateway in gateways)
{
    await TryExecuteEasyConfig(gateway);
}

编辑 2023年6月15日 11:16

SuncarContext 的添加如下所示:

services.AddDbContext<SuncarContext>(options =>
    options.UseNpgsql(Configuration.GetConnectionString("PostgresConnection"),
        sqlOption =>
        {
            sqlOption.EnableRetryOnFailure(
                5,
                TimeSpan.FromSeconds(5),
                null);
        }
    ));

编辑 2023年6月15日 11:21

Time Hosted Service 使用 StartAsync 方法,其中使用计时器调用 DoWork 方法。

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Backend.BusinessLayer.Services.Interfaces;

public class EasyConfigExecutor : IHostedService, IDisposable {
    private readonly ILogger<EasyConfigExecutor> _logger;
    private Timer? _timer;
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public EasyConfigExecutor(ILogger<EasyConfigExecutor> logger, IServiceScopeFactory serviceScopeFactory)
    {
        _logger = logger;
        _serviceScopeFactory = serviceScopeFactory;
    }

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(30));

        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        using var scope = _serviceScopeFactory.CreateScope();
        var easyConfigService = scope.ServiceProvider.GetService<IEasyConfigService>() ?? throw new ArgumentNullException();
        easyConfigService.ExecuteUnfinishedEasyConfigs();
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

编辑 2023年6月15日 14:22 - 结论

Dai 的答案确实有效。感谢您的支持。

在浏览互联网时,我还发现了一篇微软的好文章,介绍了如何在 C# 中使用定时异步后台任务。这是我现在使用的方法,可能对其他人也有帮助:Microsoft Timed asynchronous background Task

英文:

I'm trying to get my TimeHostedService with a DBContext to work but I always get the error: Cannot access a disposed context instance. So here is my Code:

First of all, I have my DoWork Method inside my Time Hosted Service. Because the class that I call (IEasyConficService) is scoped and the TimeHosted Service is singleton, I have to implement it with the scope.ServiceProvider. I think, this is so far fine.

private void DoWork(object? state)
{
    using var scope = _serviceScopeFactory.CreateScope();
    var easyConfigService = scope.ServiceProvider.GetRequiredService&lt;IEasyConfigService&gt;();
    easyConfigService.ExecuteUnfinishedEasyConfigs();
}

Now my function ExecuteUnfinishedEasyConfigs is collecting all the unfinished Gateways that I have from the Database. And then give it to the Method 'TryExecuteEasyConfig'.

public async Task ExecuteUnfinishedEasyConfigs()
{
    var gateways = _context.Gateways.Include(g =&gt; g.Machine)
        .Where(g =&gt; g.EasyConfigStatus == EasyConfigStatus.OnGoing)
        .Include(g =&gt; g.Machine)
        .ThenInclude(m =&gt; m.MachineType)
        .ToList();
    await Parallel.ForEachAsync(gateways, async (gateway, _) =&gt; await TryExecuteEasyConfig(gateway));
}

This works so far, because the _context is set in the constructor of the class with DI.

public class EasyConfigService : IEasyConfigService {
    private readonly SuncarContext _context;
    
    public EasyConfigService(SuncarContext context)
    {
        _context = context;
    }
}

But inside the TryExecuteEasyConfig method, which is also in the class EasyConfigService, I get the disposed Error from above, if I try call _context.SaveChangesAsync()

private async Task TryExecuteEasyConfig(Gateway gateway)
{
    // Do some Stuff
    await _context.SaveChangesAsync();
}

I do not know, why I can first take some Information out of the DBContext and afterwards the SaveChangesAsync() is failing. Why is the _context now disposed?

First I thought, it is because I do a Parallel.FoarEachAsync but it also happend, if I do a normal foreach. Is there any way to use a DBContext safely inside a TimeHostedService?

Edit 15.06.2023 11:12

The normal foreach is looking like:

foreach (var gateway in gateways)
{
    await TryExecuteEasyConfig(gateway);
}

Edit 15.06.2023 11:16

The SuncarContext is added as following:

services.AddDbContext&lt;SuncarContext&gt;(options =&gt;
    options.UseNpgsql(Configuration.GetConnectionString(&quot;PostgresConnection&quot;),
        sqlOption =&gt;
        {
            sqlOption.EnableRetryOnFailure(
                5,
                TimeSpan.FromSeconds(5),
                null);
        }
    ));

Edit 15.06.2023 11.21

The Time Hosted Service is Using the StartAsync Method, where I use a Timer, that is calling a DoWork Method.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Backend.BusinessLayer.Services.Interfaces;

public class EasyConfigExecutor : IHostedService, IDisposable {
    private readonly ILogger&lt;EasyConfigExecutor&gt; _logger;
    private Timer? _timer;
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public EasyConfigExecutor(ILogger&lt;EasyConfigExecutor&gt; logger, IServiceScopeFactory serviceScopeFactory)
    {
        _logger = logger;
        _serviceScopeFactory = serviceScopeFactory;
    }

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(30));

        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        using var scope = _serviceScopeFactory.CreateScope();
        var easyConfigService = scope.ServiceProvider.GetService&lt;IEasyConfigService&gt;() ?? throw new ArgumentNullException();
        easyConfigService.ExecuteUnfinishedEasyConfigs();
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

Edit 15.06.2023 14.22 - Conclusion

The Answer of Dai definitely works. Thanks for your support.

After looking through the Internet, I found also a good article from Microsoft, how to use timed asynchronous background task in c#. That's the way I'm using it now. Probably this will help some other people: Micosoft Timed asynchronous background Task

答案1

得分: 2

请阅读此文档的全部内容:https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#asynchronous-programming


问题出在你的 DoWork 方法中:你将其定义为普通的 void 方法,但它需要是一个 async Task 方法,以便你可以使用 await 来等待 TryExecuteEasyConfig 方法。

...相反,当在 easyConfigService.ExecuteUnfinishedEasyConfigs(); 内部/通过内部遇到第一个 await 时,DoWork 方法本身会立即返回。

你没有展示给我们你的 TimeHosted 服务 - 也没有真正告诉我们太多关于它,但我假设你正在使用 IHostedService(支持原样使用 async 方法)与非 async 安全的定时器回调,例如 System.Timers.TimerSystem.Threading.Timer

无论如何,你不需要使用任何 Timer 类型,只需将你的代码更改为以下内容:

public sealed class MyHostedService : IHostedService
{
    private readonly IDbContextFactory<SuncarContext> dbFactory;

    private readonly object lockObj = new object();
    private CancellationTokenSource cts = new CancellationTokenSource();
    private Task? loopTask;

    public MyHostedService(IDbContextFactory<SuncarContext> dbFactory)
    {
        this.dbFactory = dbFactory;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        lock (this.lockObj)
        {
            if (this.loopTask != null) throw new InvalidOperationException("Already running...");

            this.loopTask = Task.Run(this.RunLoopAsync);
        }

        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        // TODO: 添加更好的重入保护

        Task? rundownLoopTask; // <-- 为了线程安全,制作`this.loopTask`的本地副本。
        lock (this.lockObj)
        {
            rundownLoopTask = this.loopTask;
        }

        this.cts.Cancel();

        try
        {
            await rundownLoopTask;
        }
        catch (OperationCanceledException) // <-- 这是一个预期的异常。
        {
            // NOOP
        }
        finally
        {
            lock (this.lockObj)
            {
                this.lockObj = null;
                this.cts.Dispose();
                this.cts = new CancellationTokenSource();
            }
        }
    }

    private async Task RunLoopAsync()
    {
        while (true)
        {
            await Task.Delay(TimeSpan.FromSeconds(60));

            try
            {
                await this.RunLoopBodyOnceAsync(this.cts.Token);
            }
            catch (OperationCanceledException) when (this.cts.IsCancellationRequested)
            {
                throw; // 始终重新引发此异常以便StopAsync方法可以观察到它。
            }
            catch (Exception ex)
            {
                // TODO: 记录异常并决定是否应中止循环。
            }
        }
    }

    private async Task RunLoopBodyOnceAsync(CancellationToken cancellationToken)
    {
        using (SuncarContext db = this.dbFactory.Create())
        {
            IQueryable<Gateway> q = db.Gateways
                .Include(g => g.Machine)
                .ThenInclude(m => m.MachineType)
                .Where(g => g.EasyConfigStatus == EasyConfigStatus.OnGoing);

            List<Gateway> gateways = await q.ToListAsync(cancellationToken);

            DoStuffWithGateways(gateways);

            await db.SaveChangesAsync(cancellationToken);
        }
    }
}

希望这对你有所帮助。

英文:

You should read this document fully first: https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#asynchronous-programming


The problem is in your DoWork method: which you've defined as a normal void method when it needs to be an async Task method so that you can await the TryExecuteEasyConfig method.

...instead, what happens is the DoWork method itself will return as soon as the first await is encountered inside/via easyConfigService.ExecuteUnfinishedEasyConfigs();.

You haven't shown-us your TimeHosted service - nor really told us much about it, but I assume you're using IHostedService (which supports async methods as-is) with a non-async-safe Timer callback, such as System.Timers.Timer or System.Threading.Timer?

In any event, you don't need to use any Timer types, just change your code to this:

public sealed class MyHostedService : IHostedService
{
    private readonly IDbContextFactory&lt;SuncarContext&gt; dbFactory;

    private readonly Object lockObj = new Object();
    private CancellationTokenSource cts = new CancellationTokenSource();
    private Task? loopTask;

    public MyHostedService( IDbContextFactory&lt;SuncarContext&gt; dbFactory )
    {
        this.dbFactory = dbFactory;
    }

    public Task StartAsync( CancellationToken cancellationToken )
    {
        lock( this.lockObj )
        {
            if( this.loopTask != null ) throw new InvalidOperationException( &quot;Already running...&quot; );

            this.loopTask = Task.Run( this.RunLoopAsync );
        }

        return Task.CompletedTask;
    }

    public Task StopAsync( CancellationToken cancellationToken )
    {
        // TODO: add better reentrancy protection

        Task? rundownLoopTask; // &lt;-- Make a local copy of `this.loopTask` for thread-safety reasons.
        lock( this.lockObj )
        {
            rundownLoopTask = this.loopTask;
        }

        this.cts.Cancel();

        try
        {
            await rundownLoopTask;
        }
        catch( OperationCanceledException ) // &lt;-- This is an expected-exception.
        {
            // NOOP
        }
        finally
        {
            lock( this.lockObj )
            {
                this.lockObj = null;
                this.cts.Dispose();
                this.cts = new CancellationTokenSource();
            }
        }
    }
    
    private async Task RunLoopAsync()
    {
        while( true )
        {
            await Task.Delay( TimeSpan.FromSeconds( 60 ) );

            try
            {
                await this.RunLoopBodyOnceAsync( this.cts.Token );
            }
            catch( OperationCanceledException ) when ( this.cts.IsCancellationRequested )
            {
                throw; // Always rethrow this so the StopAsync method can observe it.
            }
            catch( Exception ex )
            {
                // TODO: Log the exception and decide if the loop should be aborted or not.
            }
        }
        
    }

    private async Task RunLoopBodyOnceAsync( CancellationToken cancellationToken )
    {
        using( SuncarContext db = this.dbFactory.Create() )
        {
            IQueryable&lt;Gateway&gt; q = db.Gateways
                .Include( g =&gt; g.Machine )
                .ThenInclude( m =&gt; m.MachineType )
                .Where( g =&gt; g.EasyConfigStatus == EasyConfigStatus.OnGoing );
                
            List&lt;Gateway&gt; gateways = await q.ToListAsync( cancellationToken );

            DoStuffWithGateways( gateways );

            await db.SaveChangesAsync( cancellationToken );
        }
    }
}

huangapple
  • 本文由 发表于 2023年6月15日 16:52:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/76480757.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定