8

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?

Ehsan Sajjad
  • 61,834
  • 16
  • 105
  • 160
Dandy
  • 1,466
  • 16
  • 31
  • The service and DB context must be of the same lifetime if the service references the DB Context. – Stanley Okpala Nwosa Apr 09 '18 at 08:52
  • @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 Apr 09 '18 at 08:54
  • 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 Apr 09 '18 at 08:57
  • @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 Apr 09 '18 at 09:00

2 Answers2

15

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
}
Evk
  • 98,527
  • 8
  • 141
  • 191
  • This seems like a great way to work around this. I hadn't considered the fact that in this case, I may have picked the wrong framework for this specific service. I hadn't considered this. Thank you! I'll try this out. – Dandy Apr 09 '18 at 09:08
  • @Dandy I've added alternative approach if you don't want to bother with scopes, not sure if asker receives notifications of answer edits. – Evk Apr 09 '18 at 10:22
  • For some reason the custom scope wasn't working with the DbContext specifically, always failed because it wasn't instantiated. Rather then figure that out, I used the Factory method for `using` and disposing. This worked like a treat! – Dandy Apr 11 '18 at 00:51
  • 'Cannot access a disposed object. Object name: 'IServiceProvider'.' – Nastya Scherbakova Mar 07 '19 at 13:18
1

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.

gatsby
  • 1,148
  • 11
  • 12