A subtle source of errors and less-than-subtle source of frustration is understanding and using service lifetimes appropriately with .NET Core dependency injection. Service lifetimes, while complicated on the surface, can help developers that need to share state across different lifetimes of an application. Typically, a long-running application has three service lifetimes that we want a service to "live" for or be shared:
- One instance shared for the lifetime of the application
- One instance shared for some request/action/activity/unit of work
- I don't care/it doesn't matter/why are you asking me
Without dependency injection, we'd typically attack these by:
- Create one static/shared instance
- Create one instance then pass it through to everything in the request/action/activity/unit of work
newit up whenever
If I'm using a framework that leverages a dependency injection container, I'll need to instead conform to the container's rules about lifetimes and avoid trying to use the "old" ways. In the .NET Core container, these three service lifetimes map to:
Typically, your application won't actually create scopes itself, and instead that's done by some middleware you don't interact with (a good thing).
So why is it so easy to get wrong? In my experience fielding MANY questions on GitHub, it all comes down to how we should decide which lifetime to use. Most mistakes come from premature optimization, and I've been guilty of this as well.
Going back to the three typical lifetimes before dependency injection, the reason a service would need a different lifetime is quite simple: State
For an injected service, my lifetime choice happens at registration time, when I'm connecting the service to its implementation. It's the implementation's state that determines the lifetime. When I refer to state, I don't mean merely the fields on the class - as the fields could be other services. I refer to "State" as "data" or information, and that scope in which that data or information needs to be shared determines the
|Scope to share state
|Stateless or should not share state
The safe default for stateless services is
Transient. If my service has state then I should look at other scopes, and make the service lifetime decision based on the scope which my state should shared.
How not to choose a
Some common ways that can mess people up:
My object is stateless, it's a waste to create objects more than once!
Tempting, but premature optimization. Subtle errors crop up if your object is stateless but takes in dependencies. If you have a stateless object with no dependencies, consider avoiding DI altogether as it's a stable dependency, and just
new it up.
OK but can I at least make it
Scoped? It's a waste!
No, you shouldn't. Your service registration also serves as documentation. When the next developer sees some implementation registered as
Scoped, the correct assumption is that the implementation has some state that should be shared for a single scope/request/action. If we poke inside and see none, it's confusing. Premature optimization is confusing, just don't.
It's a performance problem, all this waste for stateless services!
Great, prove it! Use a profiler, show that your service instantiation is causing performance issues with GC/memory.
And now that you've got the proof (that would be the 0.001% of you), then, and only then, should you choose a different service lifetime for stateless services for explicit performance reasons.
Choose your service lifetime based on the intended scope of the implementation's state.