Use DbContext in ASP .Net Singleton Injected Class
I need to access my database in a Singleton class instantiated in my Startup class. It seems that injecting it directly results in a DbContext that is disposed.
I get the following error:
Cannot access a disposed object. Object name: 'MyDbContext'.
My question is twofold: Why doesn't this work and how can I access my database in a singleton class instance?
Here is my ConfigureServices method in my Startup class:
public void ConfigureServices(IServiceCollection services)
{
// code removed for brevity
services.AddEntityFramework().AddSqlServer().AddDbContext<MyDbContext>(
options =>
{
var config = Configuration["Data:DefaultConnection:ConnectionString"];
options.UseSqlServer(config);
});
// code removed for brevity
services.AddSingleton<FunClass>();
}
Here is my controller class:
public class TestController : Controller
{
private FunClass _fun;
public TestController(FunClass fun)
{
_fun = fun;
}
public List<string> Index()
{
return _fun.GetUsers();
}
}
Here is my FunClass:
public class FunClass
{
private MyDbContext db;
public FunClass(MyDbContext ctx) {
db = ctx;
}
public List<string> GetUsers()
{
var lst = db.Users.Select(c=>c.UserName).ToList();
return lst;
}
}
Original source: https://entityframeworkcore.com/knowledge-base/51939451/how-to-use-a-database-context-in-a-singleton-service-
Since DbContext is scoped by default, you need to create scope to access it. It also allows you to handle its lifetime correctly - otherwise you'd keep instance of DbContext for a long time and this is not recommended.
public class Singleton : ISingleton
{
private readonly IServiceScopeFactory scopeFactory;
public Singleton(IServiceScopeFactory scopeFactory)
{
this.scopeFactory = scopeFactory;
}
public void MyMethod()
{
using(var scope = scopeFactory.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<DbContext>();
// when we exit the using block,
// the IServiceScope will dispose itself
// and dispose all of the services that it resolved.
}
}
}
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.
You can use this parameter on your DbContext:
ServiceLifetime.Singleton
services.AddDbContext<EntityContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DatabaseConnection")), ServiceLifetime.Singleton);
As mentioned early .AddDbContext
extension is adding it as scoped per request. So DI can not instantiate Scoped
object to construct Singleton
one.
You must create and dispose instance of MyDbContext
by yourself, it is even better because DbContext must be disposed after using as early as possible. To pass connection string you can take Configuration
from Startup
class:
public class FunClass
{
private DbContextOptions<MyDbContext> _dbContextOptions;
public FunClass(DbContextOptions<MyDbContext> dbContextOptions) {
_dbContextOptions = dbContextOptions;
}
public List<string> GetUsers()
{
using (var db = new MyDbContext(_dbContextOptions))
{
return db.Users.Select(c=>c.UserName).ToList();
}
}
}
In Startup.cs
configure DbContextOptionBuilder
and register your singleton:
var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
optionsBuilder.UseSqlServer(_configuration.GetConnectionString("DefaultConnection"));
services.AddSingleton(new FunClass(optionsBuilder.Options));
It's a little bit dirty, but works very well.