Introduction
Dependency Injection (DI) is one of those concepts that feels optional—right up until your codebase grows large enough that it becomes essential. In modern .NET applications, DI is not just a pattern; it's baked into the framework and expected as part of idiomatic design.
This article walks through how DI works in .NET, the patterns you should use, and the mistakes worth avoiding.
What Is Dependency Injection?
At its core, Dependency Injection is about inverting control. Instead of a class creating its own dependencies, those dependencies are provided externally.
Without DI
public class OrderService
{
private readonly PaymentService _paymentService = new PaymentService();
}
With DI
public class OrderService
{
private readonly IPaymentService _paymentService;
public OrderService(IPaymentService paymentService)
{
_paymentService = paymentService;
}
}
This shift enables:
- Easier testing
- Better separation of concerns
- More flexible architectures
Built-in DI in .NET
.NET provides a lightweight DI container via Microsoft.Extensions.DependencyInjection.
Registration
services.AddTransient<IMyService, MyService>();
services.AddScoped<IRepository, Repository>();
services.AddSingleton<ICache, MemoryCache>();
Resolution
In ASP.NET Core, services are automatically injected into constructors:
public class HomeController : Controller
{
private readonly IMyService _service;
public HomeController(IMyService service)
{
_service = service;
}
}
Service Lifetimes Explained
Choosing the correct lifetime is critical.
Transient
- New instance every time
- Good for lightweight, stateless services
Scoped
- One instance per request
- Ideal for database contexts
Singleton
- Single instance for entire app lifetime
- Use carefully; must be thread-safe
Common DI Patterns
1. Constructor Injection (Preferred)
The default and most recommended approach.
public class UserService
{
private readonly IRepository _repo;
public UserService(IRepository repo)
{
_repo = repo;
}
}
2. Factory Pattern
Useful when creation logic is dynamic.
services.AddTransient<Func<string, IMessageService>>(provider => key =>
{
return key switch
{
"email" => provider.GetRequiredService<EmailService>(),
"sms" => provider.GetRequiredService<SmsService>(),
_ => throw new ArgumentException()
};
});
3. Options Pattern
For configuration binding.
services.Configure<MyOptions>(configuration.GetSection("MyOptions"));
Injected as:
public class MyService
{
public MyService(IOptions<MyOptions> options)
{
}
}
4. Decorator Pattern
Wrap behavior without modifying the original implementation.
public class LoggingServiceDecorator : IService
{
private readonly IService _inner;
public LoggingServiceDecorator(IService inner)
{
_inner = inner;
}
}
Best Practices
1. Depend on Abstractions
Always inject interfaces, not concrete implementations.
2. Keep Constructors Lean
Too many dependencies often signal a violation of the Single Responsibility Principle.
3. Avoid Service Locator Anti-Pattern
// Avoid this
var service = provider.GetService<IMyService>();
This hides dependencies and makes testing harder.
4. Validate Scopes
Avoid injecting scoped services into singletons.
5. Use Composition Root
Keep service registrations centralized (e.g., in Program.cs).
Advanced Techniques
Conditional Registration
if (env.IsDevelopment())
{
services.AddSingleton<IEmailService, DebugEmailService>();
}
else
{
services.AddSingleton<IEmailService, SmtpEmailService>();
}
Open Generics
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
Assembly Scanning (Scrutor)
services.Scan(scan => scan
.FromAssembliesOf(typeof(IMarker))
.AddClasses()
.AsImplementedInterfaces()
.WithScopedLifetime());
Common Pitfalls
- Overusing singletons
- Hidden circular dependencies
- Over-injection (too many services per class)
- Misusing DI for simple object creation
When NOT to Use DI
DI is powerful, but not always necessary. Avoid it for:
- Simple, short-lived objects
- Static utility functions
- Performance-critical paths where container resolution overhead matters
Conclusion
Dependency Injection is foundational to modern .NET development. Used correctly, it enables modular, testable, and maintainable systems. Misused, it can introduce unnecessary complexity.
Focus on clarity, simplicity, and correct lifetimes—and your DI setup will scale with your application.
Further Reading
- Official Microsoft Docs on Dependency Injection
- Clean Architecture by Robert C. Martin
- Patterns of Enterprise Application Architecture by Martin Fowler
Top comments (0)