Let me start with the basics, most of which you will likely be familiar with, but let’s do it for completeness and correctness.
- With Simple Injector,
Transient
means short-lived and not cached. If service X
is registered as Transient
, and multiple components depend on X
, each get their own new instance. Even if X
implements IDisposable
, it is not tracked nor disposed of. After creation, Simple Injector forgets about Transients immediately.
- Within a specifically defined scope (e.g. a web request), Simple Injector will only create a single instance of a service registered as
Scoped
. If service X
is Scoped
, and multiple components depend on X
, all components that are created within that same scope get the same instance of X
. Scoped
instances are tracked and if they implement IDisposable
(or IAsyncDisposable
), Simple Injector calls their Dispose
method when the scope is disposed of. In the context of a web request disposal of scopes (and therefore Scoped
components) this managed for you by the Simple Injector integration packages.
- With
Singleton
, Simple injector will ensure at most one instance of that service within a single Container
instances. If you have multiple Container
instances (which you typically wouldn’t in production, but more likely during testing) each Container
instance gets its own cache with Singletons
.
The described behavior is specific to Simple Injector. Other DI Containers might have different behavior and definitions when it comes to these lifestyles:
- Simple Injector considers a
Transient
component short lived, possibly including state, while the ASP.NET Core DI (MS.DI) considers Transient components to be *stateless, living for any possible duration. * Because of this different view, with MS.DI, Transients can be injected into Singletons, while with Simple Injector you can’t. Simple Injector will give an diagnostic error when you call Verify()
.
Transients
are not disposed of by Simple Injector. That’s why you get a diagnostic error when you register a Transient
that implements IDisposable
. Again, this is very different with some other DI Containers. With MS.DI, transients are tracked and disposed of when their scope, from which they are resolved, is disposed of. There are pros and cons to this. Important con is that this will lead to accidental memory leaks when resolving disposable transients from the root container, because MS.DI will store those Transients forever.
About choosing the right lifestyle for a component:
- Choosing the correct lifestyle (
Transient
/Scoped
/Singleton
) for a component is a delegate matter.
- If a component contains state that should be reused throughout the whole application, you might want to register that component as
Singleton
-or- move the state out of the component and hide it behind a service which component can be registered as Singleton
.
- The expected lifetime of a component should never exceed that of its consumers.
Transient
is the shortest lifestyle, while Singleton
is the longest. This means that a Singleton
component should only depend on other Singletons
, while Transient
components can depend on both Transient
, Scoped
, and Singleton
components. Scoped
components should typically only have Scoped
and Singleton
dependencies.
- The previous rule, however, is quite strict, which is why with Simple Injector v5 we decided to allow
Scoped
components take a dependency on Transient
components as well. This will typically be fine in case scopes live for a short period of time (as is with web requests in web applications). If, however, you have long-running operations that operate within a single scope, but do regularly call back into the container to resolve new instances, having Scoped
instances depend on (stateful) Transients
could certainly cause trouble; that’s, however, not a very common use case.
- Failing to adhere to the rule of “components should only depend on equal or longer-lived components,” results in Captive Dependencies. Simple Injector calls them “Lifestyle Mismatches,” but it’s the same thing.
- A stateless component with no dependencies can be registered as
Singleton
.
- The lifestyle of a stateless component with dependencies, depends on the lifestyle of its dependencies. If it has components that are either
Scoped
or Transient
, it should itself be either Scoped
or Transient
. If it were registered as Singleton
, that would lead to its dependencies becoming Captive Dependencies.
- If a stateless component only has
Singleton
dependencies, it can be registered as Singleton
as well.
When it comes to selecting the correct lifestyles for your components in the application, there are two basic Composition Models to choose from, namely the Ambient Composition Model and the Closure Composition Model. I wrote a series of five blog posts on this starting here. With the Ambient Composition Model you’ll make all components in your application stateless and store state outside of the object graphs. This allows you to register almost all your components as Singleton, but it does lead to complications and likely a somewhat different design of your application.
It's, therefore, more likely that you are applying the second Composition Model: The Closure Composition Model. This is the most common model, and used by most developers and pushed by most application frameworks (e.g. ASP.NET Core DI). With the Closure Composition Model, you would typically register most of your components as Transient. Only the few components in your application that do contain state would be registered as either Scoped
or Singleton
. Although you could certainly “tune” composition by looking at the consumers and dependencies of components and decide to increase the lifestyle (to Scoped
or even Singleton
) to prevent unneeded creation of instances, downside of this is that is more fragile.
For instance, if you have a stateless component X
that depends on a Singleton
component Y
, you can make component X
a Singleton
as well. But once Y
requires a Scoped
or Transient
dependency of its own, you will not only have to adjust the lifestyle of Y
, but of X
as well. This could cascade up the dependency chain. So instead, with the Closure Composition Model, it would typically be normal to keep things “transient unless.”
About performance:
Simple Injector is highly optimized, and it would typically not make much difference if a few extra components are created. Especially if they are stateless. When running a 32 bits process, such class would consume “8 + (4 * number-of-dependencies)” bytes of memory. In other words, a stateless component with 1 dependency consumes 12 bytes of memory, while a component with 5 dependencies consumes 28 bytes (assuming a 32 bits processes; multiply this by 2 under 64 bits).
On the other hand, managing and composing Scoped
instances comes with its own overhead. Although Simple Injector is highly tuned in this regard, Scoped
instances need to be cached and resolved from the scope’s internal dictionary. This comes at a cost. This means that creating a component with no dependencies a few times as Transient
in a graph is likely faster than having it resolved as Scoped
.
Under normal conditions, you wouldn’t have to worry about the amount of extra memory and the amount of extra CPU it takes to produce those extra Transient instances. But perhaps you are not under normal conditions. The following abnormal conditions could cause trouble:
- If you violate the simple-injection-constructors rule: When a component’s constructor does more than simply storing its supplied dependencies (for instance calling them, doing I/O or something CPU intensive or memory intensive) creating extra transient instances can hurt a lot. You should absolutely try to stay away from this situation whenever possible.
- **Your application creates massive object graphs: ** If object graphs are really big, you’ll likely see certain components being depended upon multiple (or even many) times in the graph. If the graph is massive (thousands of individual instances), this could lead to the creation of hundreds or even thousands of extra objects, especially when those components have
Transient
dependencies of their own. This situation often happens when components have many dependencies. If for instance your application’s components regularly have more than 5 dependencies, you’ll quickly see the size of the object graph explode. Important to note here is that this is typically caused by a violation of the Single Responsibility Principle. Components get many dependencies when they are doing too much, taking too many responsibilities. This easily causes them to have many dependencies, and when their dependencies have many dependencies, things can explode quite easily. The real solution in that case is to make your components smaller. For instance, if you have classes like “OrderService” and “CustomerService”, they will likely have a hodgepodge of functionality and a big list of dependencies. This causes a myriad of problems; big object graphs being one of them. Fixing this in an existing application, however, is typically not easy; it requires a different design and a different mindset.
In these kinds of scenarios changing a component’s lifestyle can be beneficial for the performance of the application. You already seem to have established this in your application. In general, changing the lifestyle from Transient
to Scoped
is a pretty safe change. This is why Simple Injector v5 doesn’t complain anymore when you inject Transient
dependencies into Scoped
consumers.
This will not be the case, however, when you have a stateful Transient
component, while each consumer does expect to get its own state; in that case, changing it to Scope
would in fact break your application. However, this is not a design that I typically endorse. Although I’ve seen this type of composition a few times in the past, I never do this in my applications; IMO it leads to unneeded complexity.
TLDR;
Long story short, there are a lot of factors to consider, and perhaps a lot of places in the application where the design could be approved, but in general (especially in the context of web requests) changing the lifestyle of stateless components from Transient
to Scoped
is usually pretty safe. If this results in a big performance win in your application, you can certainly consider making Scoped
the default lifestyle.