I’ve spent a significant number of hours trying to figure this out and I think I am missing something. This is regarding .NET Core 6/8 and an ASP.Net pipeline; the glitch is with the DI’s scopes and would really appreciate any help:

There are 3 players mainly involved in this (faulty) interaction; in the startup sequence (Program.cs):

  • A database context that is registered with a ‘scoped’ lifecycle ( please note it is using an implementation-factory !).
serviceCollection.AddScoped(sp => new DatabaseContext(configuration)); //configuration is a value type for r.t. params
  • There are a bunch of service implementations registered for the same type (interface: IValidationStep) – also ‘scoped’ and a Governor service also ‘scoped’ (this is to implement some kind of workflow for a sequence of validators)
  serviceCollection.AddScoped<IValidationStep,ConcreteValidatorOne>();
  serviceCollection.AddScoped<IValidationStep,ConcreteValidatorTwo>();
  . . .
  serviceCollection.AddScoped<IValidationGovernor,ValidationGovernor>();
  • Finally, there is a worker-service (obviously a ‘singleton’) that will wait for an in-mem pub-sub channel for messages and dispatch them to the Governor: (the pub-sub channel is also a singleton services.AddSingleton(), built around a System.Threading.Channel queue).
 serviceCollection.AddHostedService<UploadStagingService>();

Now, in terms of dependencies:

  • each Validator depends on a database-context instance (to update state): constructor DI-ed
  • the Governor depends on an IEnumerable<IValidationStep> collection: constructor DI-ed
  • and, finally, the background service, *UploadStagingService *depends on an instance of the ValidationGovernor; which it will invoke in a fire & forget manner (firing up a Task).

I expect all iterations in the code below to be executed without errors; however, all dependencies are resolved correctly with the exception of the DatabaseContext that errors out with "Cannot access a disposed context instance" error:

public class UploadStagingService : BackgroundService
{
    private readonly ILogger<UploadStagingService> _logger;
    private readonly UploadProcessingQueue _uploadProcessingQueue;
    private readonly IServiceScopeFactory _scopeFactory;

    
    public UploadStagingService(ILogger<UploadStagingService> logger,  UploadProcessingQueue boundedMessageChannel,  IServiceScopeFactory scopeFactory)
    {
        _logger = logger;
        _fileProcessingQueue = boundedMessageChannel;
        _scopeFactory = scopeFactory;
    }


    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            
            await foreach (var  uploadEntry in _fileProcessingQueue.ReadAllAsync().WithCancellation(stoppingToken))
            {
                await using var _asyncServiceScope = _scopeFactory.CreateAsyncScope();
                
                var dbContext = _asyncServiceScope.ServiceProvider.GetRequiredService<DatabaseContext>();
                var validators =  _asyncServiceScope.ServiceProvider.GetServices<IValidationStep>();
                var governor =  _asyncServiceScope.ServiceProvider.GetService<IValidationGovernor>();
                try
                {
                    _ = Task.Factory.StartNew(() => governor.Complete(uploadEntry));
                }
                catch (Exception ex) {
                   ...
                }
            }
        }
        catch (OperationCanceledException)     {       _logger.LogDebug("whatever");        }
        catch (Exception ex)                 {       _logger.LogError(ex, "...");         }
        finally 
        {
            _logger.LogCritical("Fatal Error! Closing the Upload-processing Channel. This service instance should be recycled !");
            _uploadProcessingQueue.TryCompleteWriter();
        }
    }
}
1

There are 1 best solutions below

2
Jalpa Panchal On

You are facing this issue because the scope is not being used correctly; it should be created and disposed of for every unit of work , not for the lifetime of the ExecuteAsync method.

To fix this issue you could try modifying the code as shown below:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            await foreach (var uploadEntry in _uploadProcessingQueue.ReadAllAsync().WithCancellation(stoppingToken))
            {
                using var scope = _scopeFactory.CreateScope();
                var dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
                var validators = scope.ServiceProvider.GetServices<IValidationStep>();
                var governor = scope.ServiceProvider.GetRequiredService<IValidationGovernor>();
                var task = Task.Run(() => governor.Complete(uploadEntry, dbContext, validators), stoppingToken);

                try
                {
                    await task;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error processing uploadEntry: {uploadEntry}", uploadEntry);
                }
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogDebug("Upload processing cancelled.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error in ExecuteAsync of UploadStagingService.");
        }
        finally
        {
            _logger.LogCritical("Upload-processing channel is being completed. This service instance should be recycled!");
            _uploadProcessingQueue.TryCompleteWriter();
        }
    }