Dependency Injection

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.
  • 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:
  1. Constructor Injection (Most Common in .NET):
    1. 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
  1. Property (Setter) Injection:
    1. Dependencies are set through public properties after the object is constructed. Less commonly used in .NET due to potential for uninitialized dependencies.
  1. Method Injection:
    1. 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 or Startup.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)

  • 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)

  • 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)

  • 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 patternRepository patternUnit 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