I think that many developers are likely to register a DbContext like this, if they could:

Container.RegisterDbContext().AsSingleton()

Why not? A DbContext sounds like a Db Connection, so why not make it singleton and save creating and destroying it all the time?

You will quickly learn why: because DbContext does tracking. This means that as soon as you load or save an object using EF, it will "Attach" the object to the context so that when you eventually call SaveChanges(), it will know what to update.

This is great for the simple applications on the tutorial sites where people have apps which do something like this:

public IActionResult DoSomething(MyModel model)
{
    // etc...
    using ( var dbContext = new DbContext())
    {
        dbContext.Set().Attach(model);
        dbContext.SafeChanges();
    }
}

Easy peasy. The context is created, used and immediately destroyed. This is how it was designed and how it works well, in fact, DbContext is pretty lightweight and can be created and destroyed without worrying about it, it does not open and close connections, just grabs one from the connection pool.

However, in proper applications, where units of work are much larger, whether due to the complexity of the web application or using it inside a desktop application, we are rightly weary about creating and destroying a DbContext every single time we write to the database. We want to save resources and call all the stuff in one go. We probably also use dependency injection to make things testable so we can't actually just create and destroy contexts in code, this is not testable and EF is notoriously hard to mock when unit testing so really, we want to inject an IDbContext and use this in our services.

This all works OK but it then brings us back to the question of the lifetime of the DbContext. We have three choices, two choices that work! Singleton does not work. If you have a large unit of work that calls the database more than once using code like this:

var model = new MyModel();
dbContext.Set().Attach(model);
//etc..

You will get an exception. Why? Because of tracking. Even if you disable tracking, as soon as you call attach, the object is tracked and a name is generated based on the class name of the model. Call this a second time and you are told that you can't track two instances with the same name in a rather rude way!

A second, generally favoured approach is to use Lifetime Scope. This simply means that a lifetime is created at some point e.g. at the start of an HttpRequest (which most containers support automatically with middleware), and then all objects requested from the container inside this lifetime scope can get the same instance but once the request has ended, these are all disposed and the next request would get its own instances. This works great for web requests because they are generally short-lived and are unlikely to be updating different instances of the same model within a single request. This is a good balance of sharing and not allowing the tracking data to grow to a crazy level!

If your app is a desktop app, there is a good chance that Per Lifetime is too long since units of work are likely to be much longer. Although you can actually create sub-life times for sub units of work, this is likely to get complex if you are not careful so you are then going to consider Per Dependency or "Transient" lifetime, which means that each request for the object will get a new instance. Sort of wasteful but for objects where you cannot get the desired outcomes with any kind of lifetime scope, this might be what you need for DbContext.

HOWEVER, there is a gotcha. You register your DbContext as a Transient and you still get concurrency type errors! What the heck? This can easily happen if you are injecting your DbContext into another object which has a longer lifetime since the object will be obtained at construction time (usually) and then held by the parent object until it is disposed. This is especially true for Singleton parents who never die!

You can choose to make the parents transient also but if you get to the point where everything is transient, you might have a design problem! You can inject factories to allow deferred creation, which could give you a true transient within the scope of a longer lifetime but some people think factories are code smell. (If you don't know at construction time then you should pass the data into a method instead). In fact, the problem here is that we would use a factory as a workaround rather than what it should/shouldn't be used for!

I guess you know everything is correct when the registration of items appears to be a good balance: that thread-safe shared types are singletons, that items that logically belong in a scope like controllers are scoped to a lifetime and that troublesome, lightweight or data objects might be transient.

You might also have to try it and see!