Introduction
Dependency Injection (DI) is one of the most powerful features in ASP.NET Core. It helps you build loosely coupled, testable, and maintainable applications.
Most developers know how to inject services using the AddScoped, AddSingleton, or AddTransient methods—but very few truly explore the advanced patterns that make large-scale enterprise applications more modular and efficient.
In this article, we’ll go beyond the basics and explore real-world DI techniques you can apply immediately in your ASP.NET Core projects.
Table of Contents
What is Dependency Injection (Quick Recap)
Common Lifetime Scopes (Recap + Practical Use)
Advanced DI Scenarios
Conditional Service Injection
Named Service Registration
Registering Open Generics
Using Factory Methods
Module-based DI (Feature-level Registration)
Runtime Service Replacement
Handling Cross-Cutting Concerns with DI
Testing and Mocking Dependencies
Best Practices and Pitfalls
Technical Workflow (Flowchart)
1. What is Dependency Injection (Quick Recap)
Dependency Injection is a design pattern that allows objects to receive their dependencies rather than creating them directly.
ASP.NET Core has a built-in DI container, meaning we can directly inject services into controllers, middlewares, and background tasks without external libraries.
Example
public class OrderService
{
private readonly IPaymentGateway _paymentGateway;
public OrderService(IPaymentGateway paymentGateway)
{
_paymentGateway = paymentGateway;
}
public void ProcessOrder()
{
_paymentGateway.Pay();
}
}
You register dependencies like this:
services.AddScoped<IPaymentGateway, RazorpayGateway>();
services.AddScoped<OrderService>();
2. Common Lifetime Scopes (Recap + Practical Use)
| Lifetime | Description | Example Use |
|---|
| Singleton | Same instance for entire app | Caching, logging |
| Scoped | Same instance per HTTP request | Unit of Work, DbContext |
| Transient | New instance every time | Lightweight services |
Example
services.AddSingleton<ILogService, LogService>();
services.AddScoped<IRepository, Repository>();
services.AddTransient<IEmailService, EmailService>();
3. Advanced DI Scenarios
a. Conditional Service Injection
Sometimes you want to use different implementations of an interface based on configuration or environment.
if (builder.Environment.IsDevelopment())
services.AddScoped<IEmailService, DevEmailService>();
else
services.AddScoped<IEmailService, ProdEmailService>();
This allows dynamic injection depending on environment or feature toggles.
b. Named Service Registration
ASP.NET Core doesn’t natively support named services, but you can achieve it via Func factories.
services.AddScoped<DevEmailService>();
services.AddScoped<ProdEmailService>();
services.AddScoped<Func<string, IEmailService>>(sp => key =>
{
return key switch
{
"dev" => sp.GetRequiredService<DevEmailService>(),
"prod" => sp.GetRequiredService<ProdEmailService>(),
_ => throw new ArgumentException("Invalid key")
};
});
Usage
public class EmailController
{
private readonly IEmailService _emailService;
public EmailController(Func<string, IEmailService> serviceFactory)
{
_emailService = serviceFactory("prod");
}
}
c. Registering Open Generics
For repository or caching layers that are generic, register using open generics:
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
This allows:
var userRepo = serviceProvider.GetRequiredService<IRepository<User>>();
d. Using Factory Methods
You can create dynamic services with factories:
services.AddScoped<IDatabase>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
string connStr = config.GetConnectionString("DefaultConnection");
return new SqlDatabase(connStr);
});
This is useful when services depend on runtime configuration.
e. Module-Based DI (Feature-Level Registration)
In large projects, organizing DI by module helps maintain structure.
Example structure
/Features
/Orders
- OrderService.cs
- OrderRepository.cs
- OrderModule.cs
OrderModule.cs
public static class OrderModule
{
public static void Register(IServiceCollection services)
{
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IOrderRepository, OrderRepository>();
}
}
In Program.cs
OrderModule.Register(services);
This approach improves scalability and maintainability.
f. Runtime Service Replacement
In testing or maintenance mode, you may need to override services dynamically.
var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IMailer));
if (descriptor != null)
services.Remove(descriptor);
services.AddScoped<IMailer, MockMailer>();
This is helpful for A/B testing or integration tests.
4. Handling Cross-Cutting Concerns with DI
Instead of injecting logging or caching manually in every service, you can wrap it using decorators.
Example (Simple Decorator Pattern):
public class LoggingOrderService : IOrderService
{
private readonly IOrderService _inner;
private readonly ILogger<LoggingOrderService> _logger;
public LoggingOrderService(IOrderService inner, ILogger<LoggingOrderService> logger)
{
_inner = inner;
_logger = logger;
}
public void ProcessOrder()
{
_logger.LogInformation("Order processing started");
_inner.ProcessOrder();
_logger.LogInformation("Order processing completed");
}
}
You can register the decorator dynamically using Scrutor:
services.Decorate<IOrderService, LoggingOrderService>();
5. Testing and Mocking Dependencies
Dependency Injection makes unit testing simpler by replacing real implementations with mocks or stubs.
Example with xUnit + Moq
var mockPayment = new Mock<IPaymentGateway>();
mockPayment.Setup(p => p.Pay()).Returns(true);
var service = new OrderService(mockPayment.Object);
service.ProcessOrder();
6. Best Practices and Pitfalls
Do’s
Prefer constructor injection.
Use Scoped lifetime for database contexts.
Group registrations in modules.
Don’ts
Avoid service locator pattern.
Don’t inject too many dependencies (more than 4 = code smell).
Avoid creating disposable transients.
7. Technical Workflow (Flowchart)
Dependency Injection Flow
+--------------------+
| Program.cs Startup |
+---------+----------+
|
v
+--------------------------+
| Service Registration |
| (AddScoped, Singleton) |
+---------+----------------+
|
v
+--------------------------+
| ServiceProvider Builds |
+---------+----------------+
|
v
+--------------------------+
| Controller/Service Uses |
| Dependencies Injected |
+--------------------------+
Conclusion
Advanced Dependency Injection patterns in ASP.NET Core help you go beyond “just working code” to scalable, modular, and maintainable enterprise-level architecture.
By implementing techniques like module-based registration, decorators, open generics, and runtime service replacement—you can handle complexity while keeping your code clean and testable.
In real-world enterprise systems, mastering DI is not just about knowing syntax—it’s about designing for flexibility and future change.