Overview
Dependency Injection (DI) is a software design pattern and technique that deals with how components acquire their dependencies. Rather than creating dependencies internally, a class receives them from the outside — improving modularity, testability, and flexibility.
In .NET, Dependency Injection is a first-class citizen, built directly into the ASP.NET Core framework.
Purpose
Dependency Injection aims to separate the creation of a component’s dependencies from the component’s behavior, promoting Inversion of Control (IoC). The class should focus only on what it needs to do, not on how to create the things it needs.
"Don't call me, I’ll call you." — This summarizes Inversion of Control, where the framework injects required services into your class.
Some of the common dependencies needed in application are:
- Services (ILogger, DbContext, etc.)
- Repositories (ICustomerRepository)
- Configurations (IOptions)
- External integrations (IEmailService, IPaymentGateway)
Types of Dependency Injection
There are three common forms of DI:
- Constructor Injection (most common)
- Setter Injection (property-based)
- Interface Injection (less common in .NET)
1. Constructor Injection
The dependency is provided through the class constructor.
public interface IMessageService
{
string GetMessage();
}
public class EmailMessageService : IMessageService
{
public string GetMessage() => "Message via Email";
}
public class NotificationManager
{
private readonly IMessageService _messageService;
public NotificationManager(IMessageService messageService)
{
_messageService = messageService;
}
public void Notify()
{
Console.WriteLine(_messageService.GetMessage());
}
}
Registration in .NET Core
builder.Services.AddTransient<IMessageService, EmailMessageService>();
builder.Services.AddTransient<NotificationManager>();
2. Setter (Property) Injection
Dependencies are set via public properties. Not natively supported by the built-in container, but possible manually.
public class LoggerService
{
public IMessageService MessageService { get; set; }
public void Log()
{
Console.WriteLine(MessageService?.GetMessage());
}
}
In this case, MessageService
would need to be set manually after object construction.
3. Interface Injection
The dependency provides an interface to inject itself into the consumer. Rare in .NET:
public interface IInjectable
{
void Inject(IMessageService messageService);
}
Consumers then implement Inject
to receive the dependency.
Lifetimes in .NET DI
When registering services, you specify how long a service lives:
-
Transient
– new instance each time -
Scoped
– one instance per request -
Singleton
– one instance for the app lifetime
builder.Services.AddTransient<IMyService, MyService>(); // new each time
builder.Services.AddScoped<IMyService, MyService>(); // one per web request
builder.Services.AddSingleton<IMyService, MyService>(); // shared instance
Dependency Graph
.NET Core builds a dependency graph during application startup. Circular dependencies (A → B → A) are automatically detected and cause an error.
Benefits of DI
- Decoupling: Easier to replace or extend components
- Testability: Easier to write unit tests using mocks
- Reusability: Services can be reused across classes
- Maintainability: Easier to manage and evolve large applications
Real-world Example
public interface IPaymentProcessor
{
void Process(decimal amount);
}
public class StripePaymentProcessor : IPaymentProcessor
{
public void Process(decimal amount)
{
Console.WriteLine($"Processed ${amount} via Stripe.");
}
}
public class CheckoutService
{
private readonly IPaymentProcessor _paymentProcessor;
public CheckoutService(IPaymentProcessor paymentProcessor)
{
_paymentProcessor = paymentProcessor;
}
public void Checkout()
{
_paymentProcessor.Process(100);
}
}
Registration:
builder.Services.AddScoped<IPaymentProcessor, StripePaymentProcessor>();
builder.Services.AddScoped<CheckoutService>();
Usage:
var checkout = app.Services.GetRequiredService<CheckoutService>();
checkout.Checkout(); // Outputs: Processed $100 via Stripe.
Anti-Patterns to Avoid
-
Service Locator Pattern: Using
IServiceProvider.GetService()
everywhere breaks DI principles. - Too Many Dependencies: If a class has too many injected services, it's likely violating Single Responsibility Principle.
-
Mixing lifetimes improperly: Injecting
Scoped
intoSingleton
can cause unexpected behavior.
Conclusion
Dependency Injection in .NET Core encourages clean architecture by separating behavior from object creation. It's built-in, powerful, and essential for test-driven and scalable .NET applications.
By focusing on what your classes need to do, and not how they get their dependencies, your application becomes more maintainable, testable, and adaptable to change.
Top comments (0)