Dependency Injection in ASP.NET Core is one of the framework’s most powerful and foundational features. For enterprise application development, startups, and scalable SaaS platforms, DI is essential to building loosely coupled, testable, and high-performance software.
However, without a clear understanding of DI patterns, service lifetimes, and potential pitfalls, you risk performance degradation, tight coupling, and memory leaks.
In this blog, we’ll dive deep into the mechanics of Dependency Injection in ASP.NET Core, explore real-world usage patterns, highlight common mistakes, and provide practical performance tips to help you build scalable and maintainable applications.
1. What Is Dependency Injection?
Dependency Injection (DI) is a design pattern that supplies a class with its required dependencies instead of letting it create them internally.
❌ Without DI:
csharp
CopyEdit
public class OrderService
{
private readonly EmailSender _emailSender = new EmailSender();
}
✅ With DI:
csharp
CopyEdit
public class OrderService
{
private readonly IEmailSender _emailSender;
public OrderService(IEmailSender emailSender)
{
_emailSender = emailSender;
}
}
This allows better decoupling and testability—crucial for scalable and maintainable enterprise software.
2. Built-in DI Container in ASP.NET Core
ASP.NET Core includes a lightweight but powerful DI container: Microsoft.Extensions.DependencyInjection.
How to Register Services:
csharp
CopyEdit
services.AddTransient<IEmailSender, SmtpEmailSender>();
Service Lifetimes:
- Transient – A new instance every time (ideal for stateless services)
- Scoped – One instance per HTTP request (perfect for DbContext)
- Singleton – One instance for the app lifetime (must be thread-safe)
Proper configuration of service lifetimes prevents issues like data inconsistency and runtime errors.
3. Real-World DI Patterns in ASP.NET Core
✅ Constructor Injection (Most Recommended)
csharp
CopyEdit
public class PaymentService
{
private readonly ITransactionLogger _logger;
public PaymentService(ITransactionLogger logger)
{
_logger = logger;
}
}
Benefits:
- Compile-time safety
- Simplified unit testing
- Clear dependency contracts
✅ Options Pattern for Configurable Dependencies
csharp
CopyEdit
services.Configure<SmtpSettings>(Configuration.GetSection(“Smtp”));
csharp
CopyEdit
public class SmtpEmailSender : IEmailSender
{
private readonly SmtpSettings _settings;
public SmtpEmailSender(IOptions<SmtpSettings> options)
{
_settings = options.Value;
}
}
Ideal for injecting app configurations and environment-specific settings.
✅ Factory-Based Injection for Runtime Resolution
csharp
CopyEdit
services.AddTransient<Func<string, IParser>>(provider => type =>
{
return type == “csv” ? new CsvParser() : new JsonParser();
});
Useful when the correct implementation depends on runtime input.
⚠️ Service Locator Pattern (Use Sparingly)
csharp
CopyEdit
var emailSender = serviceProvider.GetRequiredService<IEmailSender>();
While flexible, it couples your class to the DI container—avoid unless in middlewares or dynamic scenarios.
4. Common Pitfalls and How to Avoid Them
❌ Injecting Scoped Services into Singleton
This leads to runtime issues and unpredictable behavior.
Fix: Don’t inject scoped services like DbContext into singletons. Refactor your services or align the lifetimes correctly.
❌ Over-Injection (God Classes)
csharp
CopyEdit
public class InvoiceService(
ICustomerRepo c, IProductRepo p, IDiscountCalculator d,
IValidator v, IEmailSender e, ICache c2, ILogger l) { … }
Too many constructor dependencies indicate a violation of the Single Responsibility Principle.
Fix: Break into smaller, focused services or use aggregators when logical.
❌ Memory Leaks from Captured Scoped Services
Holding onto scoped services (like IDbContext) in background services can lead to memory leaks.
Fix:
csharp
CopyEdit
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();
Use IServiceScopeFactory to handle scoped dependencies in background tasks.
5. Performance Optimization Tips for ASP.NET Core DI
- ✅ Constructor injection is fastest—avoid property injection.
- ✅ Limit use of reflection—scan assemblies at startup only.
- ✅ Avoid creating scopes in hot paths—e.g., middlewares or controllers.
- ✅ Use TryAdd in libraries to prevent clashing registrations:
csharp
CopyEdit
services.TryAddScoped<IMyService, MyService>();
Efficient service registration leads to faster startup and request processing times.
6. Testing with Dependency Injection
✅ Unit Testing with Mocks:
csharp
CopyEdit
var mockLogger = new Mock<ITransactionLogger>();
var service = new PaymentService(mockLogger.Object);
✅ Integration Testing with WebApplicationFactory:
csharp
CopyEdit
factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.Remove(…);
services.AddScoped<IDummyService, MockDummyService>();
});
});
Proper DI configuration helps simulate real environments effectively in tests.
7. Replacing the Default DI Container
Need advanced features like decorators, modular containers, or interceptors? Replace ASP.NET Core’s DI container with:
- Autofac
- StructureMap
- SimpleInjector
csharp
CopyEdit
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
Do this only when the built-in container can’t meet your design requirements.
8. Use Case: Scalable Multi-Tenant SaaS Platform
We partnered with a growing SaaS startup that needed full tenant isolation—including database, cache, and configuration.
Solution:
- Created a custom ITenantProvider
- Injected tenant-aware services using Scoped lifetimes
- Used middleware to resolve tenant context before controller execution
Result: A clean, DI-driven architecture that scaled to 1000+ tenants with zero performance compromise.
Conclusion: Mastering Dependency Injection in ASP.NET Core
Dependency Injection in ASP.NET Core is not just a technique—it’s an architectural mindset that encourages clean, testable, and high-performance systems. But like any tool, it must be used carefully.
✅ TL;DR:
- Prefer constructor injection over other methods.
- Always align service lifetimes correctly (Scoped vs Singleton).
- Use Options pattern and factory injection for flexibility.
- Avoid over-injection and tight coupling.
- Monitor DI performance and memory for scalability.
Additional Resources: