Adding to all the fantastic answers here. This answer will explain the different lifetime and when it is appropriate to choose them. As with many things in software development, there are no absolutes and many condition can affect the best choices, so please treat this answer as a general guidance.
ASP.NET Core ships with its own built-in Dependency Injection Container which it uses to resolve the services it needs during the request lifecycle. All framework services, for example, like Logging, configuration, Routing etc.., use dependency injection and they are registered with the Dependency Injection Container when the application host is built. Internally, the ASP.NET Core framework provides the dependencies required when activating framework components, such as Controllers and Razor pages.
A Dependency Injection Container, sometimes referred to as an Inversion of Control, or IoC Container, is a software component that manages the instantiation and configuration of objects. The dependency Injection Container is not a requirement to apply the dependency injection pattern, but as your application grows using one can greatly simply the management of dependencies, including their lifetimes. Services are registered with the container at start up and resolved from the container at runtime whenever they are required. The container is responsible for creating and disposing of instances of required services, maintaining them for a specified lifetime.
There are two primary interfaces that we code against when using the Microsoft Dependency Injection Container.
- The IServiceCollection interface defines a contract for registering and configuring a collection of service descriptors. From the IServiceCollection we build an IServiceProvider.
- IServiceProvider, defines a mechanism for resolving a service at runtime.
When registering a service with the container, a service lifetime should be chosen for the service. The service lifetime controls how long a resolved object will live for after the container has created it. The lifetime can be defined by using the appropriate extension method on the IServiceCollection when registering the service. There are three lifetimes available for use with the Microsoft Dependency Injection Container.
- Transient
- Singleton
- Scoped
The Dependency Injection Container keeps track of all the instances of services it creates, and they are disposed of or released for garbage collection once their lifetime has ended. The chosen lifetime affects whether the same service instance may be resolved and injected into more than one dependent consumer. It is crucial, therefore, to choose the lifetime of services very wisely.
Transient Services
When a service is registered as Transient, a new instance of that service is created and returned by the container every time the service is resolved. In other words, every dependent class that accepts a Transient service via injection from the container will receive its own unique instance. Because each dependent class receives its own instance, methods on the instance are safe to mutate internal state without fear of access by other consumers and threads.
Uses/ Characteristics of Transient Services
- Transient service is most useful when the service contains mutable state and is not considered thread safe.
- Using Transient services can come with a small performance cost, although that cost may be negligible for applications under general load.
- Whenever an instance is resolved, which could be as often as every request, memory for that instance must be allocated. This leads to additional work for the garbage collector in order to clean up those short lived objects.
- Transient service are easiest to reason about because instances are never shared; therefore they tend to be safest choice when it is unclear which lifetime is going to be the best option when you register a service.
Singleton Services
An application service registered with the singleton lifetime will be created only once during the lifetime of the Dependency Injection Container. In ASP.NET Core, this is equivalent to the lifetime of the application. The first time the service is required, the container will create an instance. After that, the same instance can be reused and injected into all dependent classes. The instance will remain reachable for the lifetime of the container, so it does not require disposal or garbage collection.
Uses/ Characteristics of Singleton Service
- Suppose the service is required frequently, such as per request. This can improve application performance by avoiding repeated allocation of new objects, each of which may require garbage collection only moments later.
- Additionally, if the object is expensive to construct, limiting this to a single instance can improve your application performance, as it only takes place one time when the service is first used.
- When choosing to register a service with Singleton lifetime, you must consider thread safety. Because the same instance of a Singleton service can be used by multiple requests concurrently. Any mutable state without suitable locking mechanism could lead to unexpected behaviour.
- Singleton are very well suited to functional-style services, where the methods take an input and return an output, with no shared state being used.
- A reasonable use case for Singleton lifetime in ASP.NET Core is for the memory cache, where the state must be shared for the cache to function.
- When registering services for Singleton, consider the implications of a single instance remaining allocated for the life of the application.
a. It is possible to create memory leaks is the instance holds large amount of memory
b. This can be especially problematic if memory usage can grow during the instance’s lifetime, given that it will never be released for garbage collection.
c. If a service has high memory requirements but is used very infrequently, then the singleton lifetime may not be the most appropriate choice.
Scoped Services
Scoped services sit in a middle ground between Transient and Singleton. An Instance of the scoped service lives for the length of the scope from which it is resolved. In ASP.NET Core, a scope is created within your application for each request it handles. Any scope services will be created once per scope, so that they act similarly to singleton services, but within the context of the scope. All framework components such as middleware and MVC controllers get the same instance of a scoped service when handling a particular request.
Uses/ Characteristics of Scoped Services
- The container creates a new instance per request.
- Because the container resolves a new instance of the type per request, it is not generally required to be thread safe.
- Components within the request life cycle are invoked in sequence, so the shared instance is not used concurrently.
- Scoped services are useful if multiple consumers may require the same dependency during a request.
a. An excellent example of such a case is when using Entity Framework Core. By default, the DbContext is registered with the scoped lifetime. Change tracking, therefore, applies across the entire request. Multiple components can make changes to the shared DbContext.
Avoiding Captive Dependencies
When registering your dependencies, it is crucial to ensure that the chosen lifetime is appropriate considering any dependencies that the service has of its own. This is necessary to avoid something called captive dependencies, which is where a service may live longer than is intended.
The thumb rule is, a service should not depend on a service with a lifetime shorter than its own. For example, a service registered with the singleton lifetime should not depend on a transient service. Doing so would result in the transient service being captured by the singleton service, with the instance unintentionally being referenced for the life of the application. This can lead to problematic and sometime hard to track down runtime bugs and behaviours, such as accidentally sharing non-thread safe services between threads or allowing objects to live past their intended lifetime.
To visualize this, let us consider which lifetimes can safely depend on services using another lifetime.
- Since it will be a short-lived service, a transient service can safely depend on service that have transient, scoped or singleton lifetime
- Scope services are little tricky. If they depend on a transient service, then a single instance of that transient service will live for the life of the scope across the whole request. You may or may not want this behaviour. To be absolutely safe, you might choose not to depend on transient services from scoped services, but it is safe for a scoped service to depend on other scoped or singleton services.
- A singleton service is most restrictive in terms of its dependencies, and it should not depend on transient or scoped services, but can depend upon other singleton services. Capture of scope services by singletons is one of the more dangerous possibilities. Because scoped services could be disposed of then the scope ends, It is possible that the singleton may try and access them after their disposal. This could lead to runtime exceptions in production, a really bad situation.