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();
}
}
}
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
ExecuteAsyncmethod.To fix this issue you could try modifying the code as shown below: