This week we talk about object lifetimes, why they matter, and how to choose the right one. We'll focus a little bit on Entity Framework since it's very popular and also very frequently misconfigured.
This week we talk about object lifetimes, why they matter, and how to choose the right one. We'll focus a little bit on Entity Framework since it's very popular and also very frequently misconfigured.
This week I'm announcing my new developer career coaching program, devBetter. If you're not advancing as quickly in your career as you'd like, and you could use someone in your corner pushing you to succeed and opening up doors to new opportunities, check it out at devbetter.com.
If you're not using dependency injection or following the dependency inversion principle in your code, you probably don't care much about object lifetimes. You can probably just instantiate new instances anywhere you need them and then let them be destroyed when they go out of scope. In this case, you probably have no use for an IoC or DI container. However, your code is probably also very tightly coupled, making it more difficult to test and reconfigure in the future. If it's working for you, keep at it, but if you're feeling pain from the coupling, I encourage you to check out my SOLID principles and Refactoring courses on Pluralsight to learn a different way to compose things.
If you are using DI and containers, like most developers using ASP.NET Core where it's built-in, or even ASP.NET MVC, you've probably encountered the concept of object lifetimes before. There's some variety in the nomenclature for some of the options, but using the terminology of ASP.NET Core's container, there are three main kinds of object lifetimes: transient, scoped, and singleton. Let me cover these briefly and apologies if you're already well-versed in this topic.
Transient scope refers to objects that are created any time they're requested. If you request an instance of a type from the container, and that type's scope is transient, you're getting a brand new instance. If you ask for a type and that type has a constructor parameter that's configured to be transient, that constructor parameter is going to be a brand new instance, every time.
Scoped lifetime refers to objects that, once created, live on for the duration of a given HTTP request. If you ask the container for an instance multiple times within an HTTP request, you'll get the same instance every time. Regardless of where the request to the DI container comes from within the web request, the same object instance is returned from the DI container. The first call to get an instance of the type will get a new instance; every subsequent request for that type will get this same instance.
Singleton scope is simple - there's only one instance. The first time an instance is requested, it's created, or you can create it during startup and add it to the container then. After that, the same instance is used everywhere.
So, which one should you use? Well, naturally, it depends. Since we don't have a lot of time, let's just look at a couple of scenarios that involve Entity Framework or EF Core, which I'll refer to collectively as EF. EF should be set up with a scoped lifetime, so that within a given web request, exactly one instance of an EF DbContext is used. In ASP.NET Core when configuring EF Core, the helper methods take care of this for you, so you never have to make a decision about what lifetime to use. In EF 6, you had to figure it out yourself. And either way, if you're using the repository pattern, you have to make the right choice for your repository instances, too.
It's important that the right choice is used for EF DbContexts, specifically because they track the entities they work with. As such, you can't have an entity that is tracked by multiple DbContext instances - that will cause an exception. You also typically don't want to share entity instances between requests - that can cause bugs when two requests are making changes to an entity they both think they have exclusive access to.
So let's say you configure your repository instances to be transient. That means if you have two different classes within a request, like a controller and a service, that both need the same kind of repository, they'll each get a different, newly created instance. And assuming nothing else needs a DbContext, the first instance of the repository will get a new DbContext, and the second instance of the repository will reuse the same DbContext. There's probably no need to have two separate repository instances in this case, but there shouldn't be any bugs.
Now let's say you configure your repository instances to behave as singletons. Consider the same scenario in which a given web request needs the repository first in a controller and later in a service. The very first request to the web server will result in a newly created repository instance (which will be reused) and a newly created dbcontext that's then passed to the controller. Then in that same request, when the service is created, it is passed this same repository instance, which still has the same DbContext associated with it. The request completes just fine. Now a subsequent request comes in. It will once again use the same repository instance, which still has a reference to the same DbContext. But that instance was scoped to a web request that has completed, so it's not going to work for this request. Or consider another scenario, in which two requests are occurring at the same time. Both will share the repository, and its dbcontext, so any entities created and tracked will be shared between the two requests. If one request makes a partial update, and the other request calls SaveChanges, the update will occur immediately, perhaps resulting in an error due to database constraints. This same thing can happen if you configure your dbcontext to be a singleton.
So, in the case of EF DbContexts and Repositories, the key takeaway is that their lifetimes should match, and their lifetimes in web applications should be Scoped.
For other kinds of services, especially other ORMs like NHibernate, it's important you understand exactly how these types should be configured when it comes to their object lifetimes for web scenarios.