Dependency Injection (DI) is a core design principle in modern .NET applications and a fundamental feature of ASP.NET Core. When used correctly, it improves testability, maintainability, and modularity. When misused, it can lead to hidden complexity, memory leaks, and fragile architectures. This guide focuses on practical, production-ready best practices that apply across ASP.NET Core, Minimal APIs, and layered architectures.
1. Prefer Constructor Injection (Default Approach)
Constructor injection is the most recommended and safest form of DI in .NET.
Why it is preferred:
• Makes dependencies explicit
• Ensures required services are available at object creation
• Improves testability
• Prevents runtime null dependencies
Example:
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
}
Avoid:
• Property injection (hidden dependencies)
• Service locator pattern (IServiceProvider.GetService inside classes)
2. Follow the Single Responsibility Principle
Each service should do one thing and depend only on what it needs.
Bad example:
A service that handles:
• Database access
• Logging
• Email sending
• Business logic
• Better approach:
Split into:
• Repository layer (data access)
• Domain service (business logic)
• Infrastructure services (email, logging)
This reduces coupling and improves testability.
3. Use the Correct Service Lifetime
.NET DI supports three main lifetimes:
1. Transient
Created every time requested.
• Use for lightweight, stateless services
2. Scoped
One instance per request.
• Ideal for web applications
• Common for database contexts
3. Singleton
One instance for the entire application lifecycle.
• Use for stateless shared services
Common mistake:
• Registering stateful services as singletons → leads to threading issues.
4. Avoid Captive Dependencies
A "captive dependency" occurs when a longer-lived service depends on a shorter-lived one.
Example problem: Singleton depends on Scoped service
This can cause:
• Memory leaks
• Invalid state usage
• Thread safety issues
Rule: A service should never depend on a shorter lifetime service.
5. Keep the Constructor Clean
Constructors should only:
• Assign dependencies
• Not execute logic
Avoid:
• File I/O
• Database calls
• Network operations
These belong in methods, not constructors.
6. Prefer Interfaces Over Concrete Classes
Always depend on abstractions, not implementations.
Good:
public class PaymentService
{
private readonly IPaymentGateway _gateway;
}
Bad:
public class PaymentService
{
private readonly StripeGateway _gateway;
}
Benefits:
• Easier mocking in tests
• Swappable implementations
• Better separation of concerns
7. Keep Service Registration Organized
As applications grow, Program.cs can become messy.
Best practice: Use extension methods
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
services.AddScoped<IOrderService, OrderService>();
return services;
}
}
Then in Program.cs:
builder.Services.AddApplicationServices();
8. Avoid Service Locator Pattern
Do NOT do this:
var service = provider.GetService();
Why it is bad:
• Hidden dependencies
• Harder to test
• Breaks design transparency
Instead, inject dependencies directly.
9. Be Careful with Scoped Services in Background Tasks
Scoped services (like DbContext) are not safe to use in long-running background services unless properly handled.
Correct approach:
Use IServiceScopeFactory:
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
This ensures proper lifetime management.
10. Optimize for Testability
Good DI design naturally improves testing.
Best practices:
• Use interfaces
• Avoid static dependencies
• Keep services small and focused
Testing becomes easier with frameworks like xUnit.
11. Avoid Over-Injection (God Constructors)
If a class has too many dependencies (6+ is a warning sign), it usually means:
• The class is doing too much
• Responsibilities are not properly separated
Fix:
• Split the service
• Introduce facades or coordinators
12. Use Options Pattern for Configuration
Instead of injecting raw configuration strings:
Bad:
public MyService(IConfiguration config)
Good:
Use strongly typed options:
public class EmailSettings
{
public string Host { get; set; }
}
Register:
services.Configure(configuration.GetSection("Email"));
Inject:
IOptions
13. Avoid Capturing Services in Static Fields
Never store DI services in static variables.
Why:
• Breaks lifetime management
• Causes memory leaks
• Introduces hidden state
14. Design for Minimal API and ASP.NET Core Equally
In Minimal APIs, DI is often passed directly into endpoint handlers:
app.MapGet("/orders", (IOrderService service) =>
{
return service.GetAll();
});
In ASP.NET Core controllers, DI is constructor-based.
Both approaches follow the same DI container rules—only usage differs.
15. Understand the Role of DI in the Request Pipeline
In ASP.NET Core, DI is deeply integrated into:
• Middleware pipeline
• Controllers / endpoints
• Filters
• Hosted services
This makes it more than just a pattern—it is part of the runtime architecture.
Conclusion
Dependency Injection in .NET is most effective when it is:
• Simple
• Explicit
• Well-scoped
• Interface-driven
• Lifetime-safe
The goal is not to use DI everywhere, but to use it where it improves clarity and testability without introducing unnecessary complexity.