Use DbContext in ASP .Net Singleton Injected Class
Asked Answered
F

5

89

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;
    }
}
Franciscka answered 31/3, 2016 at 11:33 Comment(5)
See this answer. An object cannot have dependencies with a shorter lifetime than itself. You can either inject a factory to create shorter lived instances, or refactor so the root of the object graph is not a singleton.Wessex
I strongly discourage you to register your DbContext as a Singleton, there are many articles on the web that tell you why it's a bad idea. Here is an answer provided by the creator of Simple Injector that tries to explain why. I would strongly suggest to use a pattern like the Repository or Unit of Work patterns.Tsar
@Tsar thanks. I have noted a warning in my working answer.Franciscka
Possible duplicate of Entity Framework Core service default lifetimeSignal
@Tsar - a DbContext IS a Unit of Work pattern.Tva
P
40

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.

Piccard answered 31/3, 2016 at 11:49 Comment(11)
Passing the container or IServiceProvider is an anti-pattern as it binds your types to a specific container (IServiceProvider in this case) and should be avoided. one should use either factory method or factory class/interface to implement this. the factory method can be implemented liked like services.AddSingleton<FunClass>( services => new FunClass(new GMBaseContext));. If you need additional application services, you can resolve them within the factory method via services.RequestService<SomeOtherDependency>() and pass it to the constructor of GMBaseContext.Gretna
yes I agree it would be better to avoid it but I think you will find places where it is used in the framework itself. ie the constructor of DBContext, but now that I looked at the latest code, it seems that IServiceProvider is no longer needed in the constructor for DBContext in the latest code so that could be avoided, but I think as of RC1 it does need it in the constructor to get a DBContext that worksPiccard
I can't instantiate my DbContext with DbContextOptions because my context derives from IdentityDbContext<ApplicationUser>, so there is no base constructor to pass DbContextOptions to.Franciscka
in the latest identity/ef code it does have a constructor that takes DBContextOptions I'm not sure about RC1 but would expect it to also have thatPiccard
another way would be to use a more advanced DI like autofaq so you could registered a named instance for your singleton separate from the main one registered per requestPiccard
I used @Gretna 's way of instantiating the Singleton, and overrode the OnConfiguring method, setting the connection string in the options on optionsbuilder. Your answer was definitely helpful but not the whole story.Franciscka
And I will definitely be questioning my design that brought me to this point. The Singleton class runs a timer that checks a process on the host machine. I don't know if the web application is the right place for that to begin with.Franciscka
This is a bad design. DbContext is not thread-safe so having DbContext instance in a Singleton object will have problem. You pass DI issue but you fail concurrency.Ironworker
It doesn't work for me, because DbContextOptions is also Scoped and can not be resolved for singleton objectForemast
@SergeiShvets at the time when I answered this DbContextOptions was injected as singleton by default, but they changed that in EFCore2 so now it is registered as scoped by default but you can change it using an optional parameter in .AddDbContext(...)Piccard
From the answer below - entityframeworkcore.com/knowledge-base/51939451/…Astromancy
J
124

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.
        }
    }
}
Jade answered 22/11, 2019 at 7:2 Comment(5)
This solution is better than the marked answer. It is easier to test.Fatherly
Thanks! Nice solution. This takes a while to get your head around if you're coming from dependency injection in the Spring Framework.Ruvalcaba
Just to add to this, if you use a class that derives from DbContext, use that class in the call or else you'll get a "No service for type 'Microsoft.EntityFrameworkCore.DbContext' has been registered." error. So the call would look like: var db = scope.ServiceProvider.GetRequiredService<MyDerivedDbContext>();Ciscaucasia
keep instance of DbContext for a long time and this is not recommended. - why?Newsom
"RE: Keeping an instance of DbContext for a long time" In a web application, this isn't an issue because once the request is completed all objects are garbage collected. In a service bus or windows service, it seems natural to dispose of each set of objects as transactions are completed...so it isn't an issue there. In a WinForm application, you "might" keep some objects alive (the only issue is memory leaks & those can be managed).Selwin
P
40

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.

Piccard answered 31/3, 2016 at 11:49 Comment(11)
Passing the container or IServiceProvider is an anti-pattern as it binds your types to a specific container (IServiceProvider in this case) and should be avoided. one should use either factory method or factory class/interface to implement this. the factory method can be implemented liked like services.AddSingleton<FunClass>( services => new FunClass(new GMBaseContext));. If you need additional application services, you can resolve them within the factory method via services.RequestService<SomeOtherDependency>() and pass it to the constructor of GMBaseContext.Gretna
yes I agree it would be better to avoid it but I think you will find places where it is used in the framework itself. ie the constructor of DBContext, but now that I looked at the latest code, it seems that IServiceProvider is no longer needed in the constructor for DBContext in the latest code so that could be avoided, but I think as of RC1 it does need it in the constructor to get a DBContext that worksPiccard
I can't instantiate my DbContext with DbContextOptions because my context derives from IdentityDbContext<ApplicationUser>, so there is no base constructor to pass DbContextOptions to.Franciscka
in the latest identity/ef code it does have a constructor that takes DBContextOptions I'm not sure about RC1 but would expect it to also have thatPiccard
another way would be to use a more advanced DI like autofaq so you could registered a named instance for your singleton separate from the main one registered per requestPiccard
I used @Gretna 's way of instantiating the Singleton, and overrode the OnConfiguring method, setting the connection string in the options on optionsbuilder. Your answer was definitely helpful but not the whole story.Franciscka
And I will definitely be questioning my design that brought me to this point. The Singleton class runs a timer that checks a process on the host machine. I don't know if the web application is the right place for that to begin with.Franciscka
This is a bad design. DbContext is not thread-safe so having DbContext instance in a Singleton object will have problem. You pass DI issue but you fail concurrency.Ironworker
It doesn't work for me, because DbContextOptions is also Scoped and can not be resolved for singleton objectForemast
@SergeiShvets at the time when I answered this DbContextOptions was injected as singleton by default, but they changed that in EFCore2 so now it is registered as scoped by default but you can change it using an optional parameter in .AddDbContext(...)Piccard
From the answer below - entityframeworkcore.com/knowledge-base/51939451/…Astromancy
V
15

You can use this parameter on your DbContext:

ServiceLifetime.Singleton

services.AddDbContext<EntityContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DatabaseConnection")), ServiceLifetime.Singleton);
Venita answered 18/4, 2021 at 23:39 Comment(7)
I don't think that making the context a Singleton is a good idea. You will have many conflict as you will have a long transactionDebug
using Singleton on DBcontext is terrible ideaCinematograph
Would it still be ok to use singleton if you have multiple context? I can think of a few scenarios like Features Flags or DB configured services (requirements).Cloy
Don't do this. No REALLY don't do this. While this may appear to work on the surface, when multiple calls come in to controllers, you'll reuse the same data context which can result in multiple threads trying to write to the database and concurrency exceptions.Irrelevant
There is one scenario where this came in handy for me. I'm building a prototype that relies on an in-memory database just to showcase the major features of my web app. Using Singleton in this case, allows me to seed the database with sample data, and allow me to demo the app without having to setup the underlying database. My point is that there may be edge cases where this is "ok", but yes I would agree that you'd never want to do this in production.Twosided
@Twosided no it's not OK even for that. You could use SQLite for this scenario. Or EF's in-memory provider, which doesn't lose its contents when the DbContext is disposedSystemic
@PanagiotisKanavos, Thanks for the clarification. You are correct... I just updated my demo, and the data in the in-memory database seems to stick around even with a Scoped context. I was under the impression that Scoped lifetime meant that the database contents would be lost when the context's lifetime ended. So that begs the question... is the in-memory database itself stored as a Singleton somewhere, then (more of a curiosity than anything)?Twosided
F
7

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.

Foremast answered 7/5, 2019 at 10:5 Comment(6)
Seems from asp.net core 2.1, DbContextOptions<MyDbContext> is added as scoped which means even if you dispose MyDbContext, it still seems that a singleton can't depend on DbContextOptions<MyDbContext> either?. Is it still wiser to pass a single DbContextOptions<MyDbContext> as in your case here?Globule
DbContextOptions is added as scoped to DI. So you can get instance of this class as dependency with per request lifetime. But there is not any link between objects are created by DI and created by yourself. Also as I know there is not any reason to stay live DbContextOption all the timeForemast
I don't think you are getting my question. the line services.AddSingleton(new FunClass(optionsBuilder.Options)); is adding a singleton of FunClass which is itself depending on DbContextOptions<MyDbContext>. So that means DbContextOptions<MyDbContext> will never be injected per request as suggested by the framework. Hope I'm making a sense.Globule
As I understood from this pull request github.com/dotnet/efcore/pull/9009 - all the reasons are related to DI cases, and no reason related to EF. Another interesting point there that we can bring back singleton scope for DbContextOptions.Foremast
Thanks for the link @Sergei I will peruse it once I get few seconds.Globule
DbContext must be disposed after using as early as possible - why?Newsom
F
6

Update

I'm fully aware that this solution is not the right way to do it. Please don't do what I did here all those years ago. In fact, don't inject a singleton DbContext at all.

Old answer

The solution was to call AddSingleton with my class being instantiated in the method parameter in my Startup class:

services.AddSingleton(s => new FunClass(new MyContext(null, Configuration["Data:DefaultConnection:ConnectionString"])));

The solution was to change my DbContext class:

public class MyContext : IdentityDbContext<ApplicationUser>
{
    private string connectionString;

    public MyContext()
    {
        
    }

    public MyContext(DbContextOptions options, string connectionString)
    {
        this.connectionString = connectionString;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        // Used when instantiating db context outside IoC 
        if (connectionString != null)
        {
            var config = connectionString;
            optionsBuilder.UseSqlServer(config);
        }
     
        base.OnConfiguring(optionsBuilder);
    }

}

As multiple people have however warned, using a DbContext in a singleton class might be a very bad idea. My usage is very limited in the real code (not the example FunClass), but I think if you are doing this it would be better to find other ways.

Franciscka answered 31/3, 2016 at 14:1 Comment(3)
Has anyone read this article? mehdi.me/ambient-dbcontext-in-ef6 I think it shows promise - for me it makes more sense for web apps to use a factory as opposed to a purely injected dbcontext - although possibly more for web applications with a lot of DI config where there maybe a performance gain is using singleton as opposed to per web request lifetimes.Carbonaceous
Note that constructor `public MyContext(DbContextOptions options, string connectionString)' ignores options parameter. Is it OK?Signal
When using a UnitOfWork pattern...you absolutely want a singleton DbContext. Otherwise, your "SubmitChanges" will not work across Repositories because each DbContext is different.Selwin

© 2022 - 2024 — McMap. All rights reserved.