What is Dependency Injection (DI)?
Dependency Injection is a design pattern and a technique used to achieve Inversion of Control (IoC) between classes and their dependencies. Instead of a class creating or managing its dependency objects internally, dependencies are provided ("injected") from the outside, typically by an external component called a DI container or injector.
- A dependency is any object that another object requires to function.
- DI enables loose coupling, making software components more modular, testable, and maintainable.
In other words, DI means that an object's collaborators or dependencies are passed to it rather than the object creating them itself.
Why Use Dependency Injection?
- Loose Coupling: Classes are independent of the concrete implementations of the dependencies they use.
- Improved Testability: Dependencies can be mocked or stubbed easily during unit testing.
- Maintainability: You can change or update dependencies without modifying the dependent class.
- Flexibility: Different implementations of a dependency can be injected based on configuration or environment.
- Encourages Single Responsibility Principle: Classes focus on their own behavior and delegate dependency creation elsewhere.
- Lifecycle & Scope Management: DI containers manage object lifetimes and scoping cleanly.
How Does Dependency Injection Work? Types of Injection
There are three common types of Dependency Injection:
- Constructor Injection (Most Common in .NET):
Dependencies are provided through a class constructor, ensuring the class cannot be instantiated without its dependencies.
public interface IService { void Serve(); } public class Service1 : IService { public void Serve() => Console.WriteLine("Service1 Called"); } public class Client { private readonly IService _service; public Client(IService service) // Dependency injected here { _service = service; } public void ServeMethod() { _service.Serve(); } } // Usage IService service = new Service1(); Client client = new Client(service); client.ServeMethod(); // Output: Service1 Called
- Property (Setter) Injection:
Dependencies are set through public properties after the object is constructed. Less commonly used in .NET due to potential for uninitialized dependencies.
- Method Injection:
Dependencies are passed directly to specific methods as parameters when needed.
Dependency Injection in .NET and ASP.NET Core
- .NET Core and ASP.NET Core have built-in, first-class DI containers.
- You register services and their implementations during application startup (
Program.cs
orStartup.cs
).
- The DI container manages object creation and injects dependencies automatically.
Example registration and usage in ASP.NET Core:
// In Program.cs or Startup.cs builder.Services.AddTransient<IService, Service1>(); // Using constructor injection in a controller or service public class MyController : ControllerBase { private readonly IService _service; public MyController(IService service) { _service = service; } public IActionResult Get() { _service.Serve(); return Ok(); } }
DI Lifetimes
When registering dependencies in ASP.NET Core's built-in Dependency Injection (DI) container, you specify their service lifetimes which control how long the container holds and shares instances of those services. There are three main lifetimes available:
1. Transient Lifetime (AddTransient
)
- A new instance of the service is created every time it is requested.
- Useful for lightweight, stateless services and when you want fresh instances.
- Example registration:
csharpservices.AddTransient<IMyService, MyService>();
- Each injection or request gets a new object.
2. Scoped Lifetime (AddScoped
)
- A single instance is created per scope. In web applications, a scope usually corresponds to a single HTTP request.
- Within one request, all injections receive the same instance, but a new request gets a new instance.
- Ideal for services that maintain state within a single request, like database contexts or units of work.
- Example registration:
csharpservices.AddScoped<IMyService, MyService>();
3. Singleton Lifetime (AddSingleton
)
- A single instance of the service is created once for the entire application lifetime and shared throughout.
- Used for stateless, thread-safe services or data that needs to be shared globally such as configuration or caching services.
- Example registration:
csharpservices.AddSingleton<IMyService, MyService>();
How to Register Dependencies with Lifetimes
In your
Program.cs
or Startup.cs
(depending on your .NET version), you register your dependencies with the desired lifetime like this:var builder = WebApplication.CreateBuilder(args); // Transient: new instance every injection builder.Services.AddTransient<ITransientService, TransientService>(); // Scoped: one instance per HTTP request builder.Services.AddScoped<IScopedService, ScopedService>(); // Singleton: one instance for the application's lifetime builder.Services.AddSingleton<ISingletonService, SingletonService>(); var app = builder.Build();
Then you inject them via constructor injection in your classes:
public class MyController : ControllerBase { private readonly ITransientService _transientService; private readonly IScopedService _scopedService; private readonly ISingletonService _singletonService; public MyController(ITransientService transientService, IScopedService scopedService, ISingletonService singletonService) { _transientService = transientService; _scopedService = scopedService; _singletonService = singletonService; } // Use the services in your methods }
Summary of Lifetimes
Lifetime | Instance Created | Use Case Example | Scope |
Transient | New instance every injection | Lightweight, stateless services | Every time requested |
Scoped | One instance per request | Database contexts, per-request services | Per HTTP request (or custom scope) |
Singleton | One instance for app lifetime | Caching, configuration, thread-safe services | Application lifetime |
Important Notes on Lifetimes
- Be careful not to inject a scoped or transient service into a singleton, as it can cause capturing short-lived dependencies in long-lived objects leading to bugs.
- Scoped lifetime is especially important for services like
DbContext
in Entity Framework Core to maintain consistency and transactions inside a request.
Key Concepts and Components
Term | Description |
Service | A class/interface representing a dependency (e.g., IService ). |
Client | The class depending on services to perform work (e.g., Client class above). |
Injector/Container | The component responsible for constructing and injecting dependencies. |
Lifetime | The scope of the service instance (Transient, Scoped, Singleton in ASP.NET Core DI). |
Benefits in .NET Programming
- Native framework support simplifies adding DI.
- Encourages interface-based design.
- Supports complex object graphs and multiple implementations.
- Facilitates centralized configuration of dependencies.
- Works seamlessly with other patterns like Options pattern, Repository pattern, Unit of Work, and Mediator.
Common Pitfalls to Avoid
- Avoid over-injecting dependencies (too many constructor parameters).
- Avoid Service Locator anti-pattern (querying service locator inside code, instead of injecting).
- Ensure dependencies are registered correctly with appropriate lifetimes to avoid memory leaks or incorrect behavior.
Summary Table
Aspect | Description |
What | A pattern for injecting dependencies rather than creating them internally |
Why | To reduce coupling, increase testability, improve maintainability |
How | Via constructor, property, or method injection |
In .NET | Built-in DI container manages services and injection |
Benefits | Loose coupling, easier testing, flexible configuration |
Common usage | Service registration in Program.cs and constructor injection |
Popular lifetimes | Transient, Scoped, Singleton |
References for Further Reading
- Microsoft Docs: Dependency injection in ASP.NET Core
- ScholarHat: What is Dependency Injection in C# With Example
- DotNetTutorials: Dependency Injection Design Pattern in C#
- Telerik Blog: Understanding Dependency Injection in ASP.NET Core