7
votes

I have a RabbitMQ Singleton that is working fine, but has a dependency on a scoped service whenever a message arrives:

consumer.Received += _resourcesHandler.ProcessResourceObject; //Scoped Service

My services are registered like so:

services.AddScoped<IHandler, Handler>();
services.AddSingleton<RabbitMqListener>();

The scoped services constructors uses DI for the Db Context:

private readonly ApplicationDbContext _appDbContext;

public ResourcesHandler(ApplicationDbContext appDbContext)
{
    _appDbContext = appDbContext;
}

This scoped service calls the Db Context in order to insert properties to the database on receipt of a message.

However, because the scoped service has a different lifetime, startup is failing.

Is there a better way to do this? I could make the scoped service a singleton, but then I'd have the problem of using DbContext as a dependancy.

What's the "protocol" in DI for calling the dbContext in singleton services?

I could use a using statement to make sure its disposed, but then I'd have to pass the DbContextOptions using DI instead. Is this the only way to achieve this?

2
The service and DB context must be of the same lifetime if the service references the DB Context. - Stanley Okpala Nwosa
@StanleyOkpalaNwosa Yes. I understand this. My problem is, in my situation, the MQ listener must be a singleton service in order to collect messages. It's the dependancy of the singleton that uses ApplicationDbContext. Is there a way to call DbContext inside of a Singleton safely? - Dandy
Do you mean Scoped or Transient? There is an AddScoped method that I don't see in your code. Scoped only makes sense for requests that come in through the ASP.NET Core pipeline. I'm unsure if RabbitMq messages do that. Edit: I see that you changed AddTransient to AddScoped... - Hans Kilian
@HansKilian Refresh! I was testing with Transient, but changed back to scoped. Apologies. To expand - The RabbitMQ listener in my fetches the registered service on startup, so it has to be singleton to correctly register on startup. - Dandy

2 Answers

9
votes

One way is to create scope yourself. Usually asp.net core creates scope for you when request starts and closes scope when request ends. But in your case - rabbitmq message consumption is not related to http requests at all. You can say though, that every message processing represents its own scope.

In such case, inject IServiceProvider to RabbitMqListener (represented as _provider private field below) and then:

private void OnMessageReceived(Message message) {
    using (var scope = _provider.CreateScope()) {
        var handler = scope.ServiceProvider.GetRequiredService<IHandler>();
        handler.ProcessResourceObject(message);
    }
}

Alternative could be to register ApplicationDbContext factory in container (in addition to regular scoped registration). Factory will return new instance of ApplicationDbContext and that will be callers responsibility to dispose it. For example:

services.AddSingleton<Func<ApplicationDbContext>>(() =>
{
    var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
    optionsBuilder.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
    return new ApplicationDbContext(optionsBuilder.Options);
});

Then you can register IHandler as singleton (and not scoped like now) and inject Func<ApplicationDbContext> in its constructor:

private readonly Func<ApplicationDbContext> _appDbContextFactory;

public ResourcesHandler(Func<ApplicationDbContext> appDbContextFactory)
{
    _appDbContextFactory = appDbContextFactory;
}

Then whenever you need to process message in handler - you manage context yourself:

using (var context = _appDbContextFactory()) {
    // do stuff
}
1
votes

I think that if you create a ContextFactory and ask it to for the Context would be a good approach.

You can just register your new Factory like

services.AddSingleton<ContextFactory>();

And inject on the constructor of your handler.

And then you can : _yourService.GetContext(); and use it.

Your factory should has the logic about how to create the context and will be isolated of the rest. Any time you need to use the context, you should call the factory.

Remember as long is a Singleton, you should not use states insides.

Any way if you want to use states just register as Transient for example.

**EDIT : remember to return always NEW instance of the context.