satellite satellite
satellite satellite

Reputation: 963

Dependency Injection lifetimes for a Factory

I have a .Net Core 3.1 web API, and I have the followings dependencies:

  1. As Scoped SomeDbContext (using AddDbContextPool).
  2. As Transient SomeDbRepository, it recive the above SomeDbContext.
  3. As Transient SomeService, it recive the above SomeDbRepository.
  4. As Singleton a factory for SomeService.

Here the code for injecting the dependencies in Startup.cs

services.AddDbContextPool<SomeDbContext>(options => options.UseSqlServer("connectionsString"));

services.AddTransient<SomeDbRepository>();// recive SomeDbContext
services.AddTransient<SomeService>();// recive SomeDbRepository

//Factory for SomeService
services.AddSingleton<Func<SomeService>>(provider => () =>
{
    return provider.GetService<SomeService>();
});    

API Controller

//API Controller
public SomeController(Func<SomeService> factory)
{
    _someService = factory.Invoke();
} 

When I call my API in a multi thread enviroment I get the error:

Error: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext

Why is my DbContext used by different threads? It suposse the Factory returns a new scoped DbContext per request. I use (Invoke) the Factory only one time por request.

If I change the lifetime of the Factory, from Sinlgeton to Scoped, it works ok. But I dont know Why? Why the lifetime of the Factory affects? Could someone explain me why it works that way. Thank a lot guys!!

Upvotes: 3

Views: 1514

Answers (1)

Steven
Steven

Reputation: 172606

The IServiceProvider parameter of the delegate of a Singleton registration provides a reference to the root scope. Scoped (and IDisposable Transient) services are tracked in the scope they are resolved from and since the root provider has its own scope, resolving from the root IServiceProvider causes Scoped (and IDisposable Transient) services to be cached for the lifetime of that root provider. This typically means: they live for as long as the application runs.

What this means is that, in the context of the MS.DI container, it is typically a bad idea to register delegates as Singleton that resolve from the IServiceProvider parameter, because it causes resolved Scoped depedendencies to accidentally become Singletons (they become accidental Captive Dependencies). With IDisposable Transients it becomes even more implicit, because resolving them from the root provider could even cause memory leaks, because they will only get released when the application shuts down.

Although it is fine to register a delegate as Singleton when it only resolves other Singletons (or non-disposable Transients), this is typically quite fragile, because it's easy to change this in the future, in a way that accidentally causes trouble.

Instead, with MS.DI, it's best to register your factory delegates as Scoped:

services.AddScoped<Func<SomeService>>(provider => () =>
{
    return provider.GetService<SomeService>();
}); 

Note that this advise is specific to MS.DI and might not be appliable to other DI Containers.

Upvotes: 4

Related Questions