Software-Engineering DI DI-Lifetimes
# What?
Dependency Injection (DI) is a software pattern for achieving Inversion of Control (IoC).
Meaning that control is delegated to an external framework/container.
# IoC
Inversion of Controll
The direction of dependency within the application should be in the direction of abstraction, not implementation details. Most applications are written such that compile-time dependency flows in the direction of runtime execution, producing a direct dependency graph. That is, if class A calls a method of class B and class B calls a method of class C, then at compile time class A will depend on class B, and class B will depend on class C.
# IoC (2)
Applying the dependency inversion principle allows A to call methods on an abstraction (Interface/Base-Class) that B implements, making it possible for A to call B at run time, but for B to depend on an interface controlled by A at compile time (thus, inverting the typical compile-time dependency). At run time, the flow of program execution remains unchanged, but the introduction of interfaces means that different implementations of these interfaces can easily be plugged in.
We use interfaces, that other Classes get independent of others. The classes should only be dependent on the Interface.
Dependency inversion is a key part of building loosely coupled applications, since implementation details can be written to depend on and implement higher-level abstractions, rather than the other way around. The resulting applications are more testable, modular, and maintainable as a result. The practice of dependency injection is made possible by following the dependency inversion principle.
# Dependency Injection in Asp.Net Core
Asp.Net Core delivers by default DI within it.
A dependency is an object that another object depends on. Examine the following MyDependency
class with a WriteMessage
method that other classes depend on:
A class can create an instance of the MyDependency
class to make use of its WriteMessage
method. In the following example, the MyDependency
class is a dependency of the IndexModel
class:
The class creates and directly depends on the MyDependency
class. Code dependencies, such as in the previous example, are problematic and should be avoided for the following reasons:
- To replace
MyDependency
with a different implementation, theIndexModel
class must be modified. - If
MyDependency
has dependencies, they must also be configured by theIndexModel
class. In a large project with multiple classes depending onMyDependency
, the configuration code becomes scattered across the app. - This implementation is difficult to unit test.
# DI applied
Dependency injection addresses these problems through:
- The use of an interface or base class to abstract the dependency implementation.
- Registration of the dependency in a service container. ASP.NET Core provides a built-in service container, IServiceProvider. Services are typically registered in the app’s
Program.cs
file. - Injection of the service into the constructor of the class where it’s used. The framework takes on the responsibility of creating an instance of the dependency and disposing of it when it’s no longer needed.
The sample app registers the IMyDependency
service with the concrete type MyDependency
. The AddScoped method registers the service with a scoped lifetime, the lifetime of a single request.
The IMyDependency
service is requested and used to call the WriteMessage
method. Every type that is registered in the ServiceProvider can request dependencies.
By using the DI pattern, the controller or Razor Page:
- Doesn’t use the concrete type
MyDependency
, only theIMyDependency
interface it implements. That makes it easy to change the implementation without modifying the controller or Razor Page. - Doesn’t create an instance of
MyDependency
, it’s created by the DI container.
# Service lifetimes
Services can be registered with one of the following lifetimes:
- Transient
- Scoped
- Singleton
# Transient lifetime
New instance for every request - independent of other requests
- when the request is over - instance gets disposed
Transient lifetime services are created each time they’re requested from the service container. This lifetime works best for lightweight, stateless services. Register transient services with AddTransient.
# Scoped lifetime
One instance for requests in the same scope (“Anwendungsbereich”); requests share the same instance
- when 2 clients get responds - they get two different instances
- when the request is over - instance gets disposed
For web applications, a scoped lifetime indicates that services are created once per client request. Register scoped services with AddScoped.
When using Entity Framework Core, the AddDbContext extension method registers DbContext
types with a scoped lifetime by default.
# Scoped lifetime - Warning
Do not resolve a scoped service from a singleton and be careful not to do so indirectly, for example, through a transient service. It may cause the service to have incorrect state when processing subsequent requests. It’s fine to:
- Resolve a singleton service from a scoped or transient service.
- Resolve a scoped service from another scoped or transient service.
# Singleton lifetime
One instance, that is shared for all requests in the app.
- does not get disposed
Singleton lifetime services are created either:
- The first time they’re requested - a refresh does not change them.
- By the developer, when providing an implementation instance directly to the container. This approach is rarely needed.
Example: Multiplayer-Game: when waiting for 2 or more players within one instance.
Every subsequent request of the service implementation from the dependency injection container uses the same instance. If the app requires singleton behavior, allow the service container to manage the service’s lifetime. Don’t implement the singleton design pattern and provide code to dispose of the singleton. Services should never be disposed by code that resolved the service from the container. If a type or factory is registered as a singleton, the container disposes the singleton automatically.
Register singleton services with AddSingleton. Singleton services must be thread safe and are often used in stateless services.
# Registration
You can also register a type without an interface.
# Factory based dependency creation
Assume you have the following class. The class needs a prefix and suffix for instantiation. These values should be provided on startup from a configuration file.
Add the following to your appsettings.json
Create a class for these options:
Register these options:
Add a factory method to your service provider to register the LabelGenService
and define how it should be instantiated.
# DI Example
bag = DI-Container water bottle = implementation of services
Image a person wants to go hiking. For hiking he takes a bag with him. This bag is used because he wants to store a water bottle, money and other stuff in it.
So he is able to use his filled bag in order to drink water.
# Final Comparison
# Without DI
every page creates a new object
- not scalable
for changes, we would have to change every Instance of the object
# With DI
framework looks in DI-Container for the IEmail. Then it creates the object and passes it to the page.
for changes, we just have to modify the builder method for the new implementation.
# DI-Lifetime Example
We have a Web-Application that generates a GUID (Globally Unique Identifier), when the method GetGuid() is called.
Here we have (for each 2 objects):
- 1 Transient Service
- 1 Scoped Service
- 1 Singleton Service
When reloading our application:
Transient
- the GUID for both changes, their GUID does also not matchScoped
- the GUID for both changes, but their GUID matchesSingleton
- the GUID stays for both the same