Use Dbcontext in Asp .Net Singleton Injected Class

Use DbContext in ASP .Net Singleton Injected Class

The reason it doesn't work is because the .AddDbContext extension is adding it as scoped per request. Scoped per request is generally what you want and typically save changes would be called once per request and then the dbcontext would be disposed at the end of the request.

If you really need to use a dbContext inside a singleton, then your FunClass class should probably take a dependency on IServiceProvider and DbContextOptions instead of directly taking a dependency on the DbContext, that way you can create it yourself.

public class FunClass
{
private GMBaseContext db;

public FunClass(IServiceProvider services, DbContextOptions dbOptions)
{
db = new GMBaseContext(services, dbOptions);
}

public List<string> GetUsers()
{
var lst = db.Users.Select(c=>c.UserName).ToList();
return lst;
}
}

That said, my advice would be to carefully consider whether you really need your FunClass to be a singleton, I would avoid that unless you have a very good reason for making it a singleton.

How to use DbContext in a singleton class?

There are several solutions to solve this. Here are the options I could think of:

  • Make the EUMemberChecker scoped, but pull the cache part out of the class, and inject it as a Singleton service.
  • Make EUMemberChecker part of your Composition Root to allow injecting the Container instance (e.g. IServiceProvider) into that class. This allows creating a scope instance from which you can resolve, for instance, the DbContext. If the class contains business logic, it'd be good to extract that from the class and inject that into the class as well; you wish to keep the amount of code inside your Composition Root as small as possible.
  • Ignore and suppress the warning, as you seem to be sure that this doesn't cause any problem in your specific case. Note that with MS.DI, there likely isn't an easy way to suppress this error.
  • Construct the DbContext manually inside the EUMemberChecker at the time you create the cache. This might mean you need to inject configuration values into EUMemberChecker, such as the connection string, which is something DbContext obviously needs.
  • Load the data from the database before making your container registrations and supply this cache manually upon registration. e.g.: services.AddSingleton(c => new EUMemberChecker(loadedMembers)).
  • Initialize EUMemberChecker directly at startup by calling some sort of Initialize method. Either the loaded members can be supplied to the method, or you can pass in the DbContext so that Initialize can do the querying internally. This DbContext can be resolved from the container at startup, probably by resolving it from a manually created scope.

Which option is best, depends on a lot of implementation details, so you will have to decide which one best suits your needs.

Singleton service and EF Core dbContext

Dunno why would you not use the automatic Dependecy Injection at your first code

Singleton are created upon app start. And as long as the init method is called inside the constructor it will run. So this code will work on your case already

public void ConfigureServices(IServiceCollection services)
{
string connection = Configuration.GetConnectionString("ConnectionDB");
services.AddDbContext<DataBaseContext>(options => options.UseSqlServer(connection), ServiceLifetime.Transient, ServiceLifetime.Singleton);

services.AddSingleton<Project>();
}

But anyway if you insist on instantiating the Project class then you can use this. Get the DBContext using ServiceProvider.

public void ConfigureServices(IServiceCollection services)
{
// AddDbContext
var sp = services.BuildServiceProvider();
var dbContext = sp.GetRequiredService<DbContext>();
Project project = new Project(dbContext);
services.AddSingleton(typeof(Project), project);
}

Is it ok to inject DbContext as singleton when there is only read operations?

Entity Framework developers explicitly say that DbContext is not thread safe for any operations performed on it, not just write (add, save changes etc) operations, and you should just believe them on that if you don't want to spend days debugging mysterious failures one day.

Even on read operations, EF can perform in-memory write operations on it's internal structures which are not thread safe, you cannot be sure it doesn't do that in any given case. For example, from documentation taking about processing of result set returned by a query:

If the query is a tracking query, EF checks if the data represents an
entity already in the change tracker for the context instance

So if query is tracking query - it checks change tracker for current instance for already existing entity of this type with same key, which means if such entity doesn't exist - it puts it into change tracker. This is write operation, so not safe.

You can say, well, I'll just use AsNoTracking() then. But here is another issue, about conncurrent AsNoTracking queries - EF won't even allow you to execute them anyway. Library maintainer says:

Concurrent use of the same DbContext is not possible - and not just
for tracked queries. Specifically, a DbContext has an underlying
DbConnection to the database, which cannot be used concurrently. There
are other various components at work under the hood which don't
support multithreading.

However, there's nothing wrong with instantiating several DbContexts
and executing queries on them - whether tracking or non-tracking. That
should get you the behavior you're looking for. If you run into any
further issues don't hesitate to post back.

So there are undocumented internal components in play which are not thread safe and you cannot be sure while doing anything on DbContext from multiple threads that you won't hit such components.

Entity Framework Core with dependency injection with singletons

When resolving a singleton with MS.DI, that singleton and all its dependencies are resolved from the root scope/container. This is why you experience the behavior where resolving the MyDbContext from an injected IServiceProvider always gives the same instance; that scoped instance is scoped to the container. In other words, that scoped instance implicitly became a singleton.

MS.DI's Closure Composition Model makes it very hard to resolve scopes within singletons (without manually managing scopes through ambient state), because scopes are not available ubiquitously through ambient state, as is done using the Ambient Composition Model.

In practice, there are two options here:

  1. Either you shorten the lifestyle of your singleton component to either Transient or Scoped
  2. You start and manage an IServiceScope from within the component's methods and resolve the scoped component from such scope. For instance:
    public class MySingleton
    {
    private readonly IServiceProvider provider;
    public MySingleton(IServiceProvider provider)
    {
    this.provider = provider;
    }

    public void SomeMethod()
    {
    using (var scope = this.provider.CreateScope())
    {
    scope.ServiceProvider.GetRequiredInstancce<MyDbContext>();
    }
    }
    }

.Net Core: How do I initialize a singleton that needs a DBContext?

There is no one single solution to your problem. At play are different principles, such as the idea of preventing Captive Dependencies, which states that a component should only depend on services with an equal or longer lifetime. This idea pushes towards having a MyLookup class that has either a scoped or transient lifestyle.

This idea comes down to practicing the Closure Composition Model, which means you compose object graphs that capture runtime data in variables of the graph’s components. The opposite composition model is the Ambient Composition Model, which keeps state outside the object graph and allows retrieving state (such as your DbContext) on demand.

But this is all theory. At first, it might be difficult to convert this into practice. (Again) in theory, applying the Closure Composition Model is simple, because it simply means giving MyLookup a shorter lifestyle, e.g. Scoped. But when MyLookup itself captures state that needs to be reused for the duration of the application, this seems impossible.

But this is often not the case. One solution is to extract the state out of the MyLookup, into a dependency that holds no dependencies of its own (or only depends on singletons) and than becomes a singleton. The MyLookup can than be 'downgraded' to being Scoped and pass the runtime data on to its singleton dependency that does the caching. I would have loved showing you an example of this, but your question needs more details in order to do this.

But if you want to leave the MyLookup a singleton, there are definitely ways to do this. For instance, you can wrap a single operation inside a scope. Example:

public class MyLookup : IMyLookup
...
public MyLookup (IMemoryCache memoryCache, IServiceScopeFactory scopeFactory)
{
_cache = memoryCache;
_scopeFactory = scopeFactory;
}

private List<string> QueryNamesFromDB()
{
using (var scope = _scopeFactory.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<MyDbContext>();
var allNames = context.Persons.Select(e => e.Name).Distinct().ToList<string>();
return allSInames;
}
}
}

In this example, the MyLookup is injected with a IServiceScopeFactory. This allows the creation (and destruction) of an IServiceScope in a single call. Downside of this approach is that MyLookup now requires a dependency on the DI Container. Only classes that are part of the Composition Root should be aware of the existence of the DI Container.

So instead, a common approach is to inject a Func<MyDbContext> dependency. But this is actually pretty hard with MS.DI, because when you try this, the factory comes scoped to the root container, while your DbContext always needs to be scoped. There are ways around this, but I'll not go into those, due to time constrains from my side, and because that would just complicate my answer.

To separate the dependency on the DI Container from your business logic, you would either have to:

  • Move this complete class inside your Composition Root
  • or split the class into two to allow the business logic to be kept outside the Composition Root; you might for instance achieve this using sub classing or using composition.

Getting DbContext when resolving a singleton

AddDbContext defaults to using a scoped lifestyle:

Scoped lifetime services (AddScoped) are created once per client request (connection).

The reason an error is being thrown is that you're attempting to obtain an instance of MyContext from outside of a request. As the error message suggests, it is not possible to obtain a scoped service from the root IServiceProvider.

For your purposes, you can create a scope explicitly and use that for your dependency resolution, like so:

services.AddSingleton<IMyModel>(sp =>
{
using (var scope = sp.CreateScope())
{
var dbContext = scope.ServiceProvider.GetService<MyContext>();
var lastItem = dbContext.Items.LastOrDefault();
return new MyModel(lastItem);
}
});

This code above creates a scoped IServiceProvider that can be used for obtaining scoped services.

How to consume a Scoped service from a Singleton?

A good way to use services inside of hosted services is to create a scope when needed. This allows to use services / db contexts etc. with the lifetime configuration they are set up with. Not creating a scope could in theory lead to creating singleton DbContexts and improper context reusing (EF Core 2.0 with DbContext pools).

To do this, inject an IServiceScopeFactory and use it to create a scope when needed. Then resolve any dependencies you need from this scope. This also allows you to register custom services as scoped dependencies should you want to move logic out of the hosted service and use the hosted service only to trigger some work (e.g. regularly trigger a task - this would regularly create scopes, create the task service in this scope which also gets a db context injected).

public class MyHostedService : IHostedService
{
private readonly IServiceScopeFactory scopeFactory;

public MyHostedService(IServiceScopeFactory scopeFactory)
{
this.scopeFactory = scopeFactory;
}

public void DoWork()
{
using (var scope = scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();

}
}

}


Related Topics



Leave a reply



Submit