Dependency Injection
Dependency Injection
What?
Dependency Injection (DI) is a software pattern for achieving Inversion of Control (IoC).
IoC
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 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.
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
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
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.
In apps that process requests, transient services are disposed at the end of the request.
Scoped lifetime
For web applications, a scoped lifetime indicates that services are created once per client request (connection). Register scoped services with AddScoped.
In apps that process requests, scoped services are disposed at the end of the request.
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
Singleton lifetime services are created either:
- The first time they’re requested.
- By the developer, when providing an implementation instance directly to the container. This approach is rarely needed.
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.