Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Changing whether a service is injected as scoped or transient via the .Net Core DI container at runtime?

We have several classes that depend on Entity Framework 6 in our application. As such, we inject our DbContext into various areas. However, certain modules implement multithreaded methods that require DbContext is injected as a transient service to prevent any threading issues. Other modules are capable of being strung together and saved wholesale by simply calling SaveChanges on any sub-module or module that receives the same shared DbContext. However, this approach requires the DbContext be added as a scoped service.

Aside from building some subclass or interface, that simply inherits from my DbContext, is there any way to dynamically determine whether a class gets a scoped version or transient version of a given service?

An example of the subclassed context may look something like

public class TransientDbContext : DbContext {}
public class ScopedDbContext : DbContext {}

// in services
services.AddTransient<TransientDbContext>();
services.AddScoped<ScopedDbContext>();

Which works, but I'm looking for something a bit more dynamic where I could potentially pass a parameter to indicate that a class should utilize a shared context.

For some additional context, image I have the following interfaces

public interface IRepository<TEntity> 
{
    void Add(TEntity entity);
    Task SaveAsync(CancellationToken token = default);
}

public interface IUserManager 
{
    Task AddAsync(User user, bool commitChanges = true, CancellationToken = default);
}

public interface IUserPhoneNumberManager 
{
    Task AddAsync(UserPhoneNumber number, bool commitChanges, CancellationToken token = default)
}

And behind the scenes, I may have the follow concrete implementations

public class UserRepository<User> : IRepository<User>
{
    private readonly DbContext _dbContext;
    public UserRepository(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public void Add(User entity) 
    {
        _dbContext.Users.Add(entity);
    }

    public Task SaveAsync(CancellationToken token = default)
    {
        return _dbContext.SaveChangesAsync(token);
    }
}

public class UserPhoneNumberRepository<UserPhoneNumber> : IRepository<UserPhoneNumber>
{
    private readonly DbContext _dbContext;
    public UserRepository(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public void Add(UserPhoneNumber entity) 
    {
        _dbContext.UserPhoneNumbers.Add(entity);
    }

    public Task SaveAsync(CancellationToken token = default)
    {
        return _dbContext.SaveChangesAsync(token);
    }
}

Now in some cases, I want the underlying repositories to be injected with a singular scoped context, and other times I want a transient context. Those transient contexts will obviously commit their own changes when used. But the scoped contexts will commit their changes as a singular unit.

like image 241
JD Davis Avatar asked Oct 30 '25 06:10

JD Davis


1 Answers

I think the core of your issue lies in the following observation:

certain modules implement multithreaded methods that require DbContext is injected as a transient service to prevent any threading issues.

This implies that your application code itself is responsible for handling multi-threadedness; you are likely starting new threads or tasks. This is something you should prevent.

Instead, only your Composition Root should know about multitheadedness and should spin off new threads. This centralizes the knowledge around thread safety. But not only that, but many components are not thread-safe and only the Composition Root should be aware of which components are and aren't. A component itself, should always just call its dependencies in a sequential way and assume there is just a single instance of that dependency.

This means that when you are starting parallel operations, you should go back to the Composition Root to let it resolve a new object graph. The Composition Root could then decide to inject new instances of components into the graph (for instance your DbContext).

When you apply this way of working, you won't need to have a transient and a scoped version of your DbContext any longer.

For more information, see: working with DI in multi-threaded applications. My book DI PP&P does contain some material explaining this.

like image 197
Steven Avatar answered Oct 31 '25 21:10

Steven