Introduction to Dependency Injection
Dependency Injection (DI) is a design pattern that implements Inversion of Control (IoC) for resolving dependencies. It’s a fundamental technique for writing maintainable, testable, and loosely coupled code. In modern C# development, especially with ASP.NET Core, DI has become a core part of application architecture.
This guide covers DI concepts, IoC containers, implementation patterns, best practices, and advanced scenarios.
Understanding Dependency Injection
What is Dependency Injection?
Dependency Injection is a technique where an object receives its dependencies from external sources rather than creating them itself.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| // WITHOUT Dependency Injection
public class OrderService
{
private readonly OrderRepository _repository;
public OrderService()
{
// Tight coupling - creates its own dependency
_repository = new OrderRepository();
}
public void CreateOrder(Order order)
{
_repository.Save(order);
}
}
// WITH Dependency Injection
public class OrderService
{
private readonly IOrderRepository _repository;
// Dependency injected via constructor
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
public void CreateOrder(Order order)
{
_repository.Save(order);
}
}
|
Benefits of Dependency Injection
1. Loose Coupling: Components depend on abstractions, not concrete implementations.
2. Testability: Easy to mock dependencies in unit tests.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| [TestMethod]
public void CreateOrder_SavesOrderSuccessfully()
{
// Arrange
var mockRepository = new Mock<IOrderRepository>();
var service = new OrderService(mockRepository.Object);
var order = new Order { Id = 1 };
// Act
service.CreateOrder(order);
// Assert
mockRepository.Verify(r => r.Save(order), Times.Once);
}
|
3. Maintainability: Changes to dependencies don’t require changes to dependent classes.
4. Flexibility: Easy to swap implementations without modifying code.
Types of Dependency Injection
1. Constructor Injection
The most common and recommended approach.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class ProductService
{
private readonly IProductRepository _repository;
private readonly ILogger<ProductService> _logger;
// Dependencies injected via constructor
public ProductService(
IProductRepository repository,
ILogger<ProductService> logger)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<Product> GetProductAsync(int id)
{
_logger.LogInformation("Fetching product {ProductId}", id);
return await _repository.GetByIdAsync(id);
}
}
|
Advantages:
- Dependencies are explicit and required
- Immutability (readonly fields)
- Easy to test
- Prevents partially constructed objects
2. Property Injection
Used for optional dependencies.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public class EmailService
{
private readonly IEmailSender _sender;
public EmailService(IEmailSender sender)
{
_sender = sender;
}
// Optional dependency via property
public ILogger Logger { get; set; }
public async Task SendEmailAsync(string to, string subject, string body)
{
Logger?.LogInformation("Sending email to {Recipient}", to);
await _sender.SendAsync(to, subject, body);
}
}
|
Disadvantages:
- Dependencies not immediately obvious
- Can be set to null
- Less preferred than constructor injection
3. Method Injection
Dependencies passed as method parameters.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public class ReportGenerator
{
public Report Generate(IDataSource dataSource, IReportTemplate template)
{
var data = dataSource.GetData();
return template.Format(data);
}
}
// Usage
var generator = new ReportGenerator();
var report = generator.Generate(
new DatabaseDataSource(),
new PdfTemplate()
);
|
Use cases:
- Different dependency needed per method call
- Dependency not needed for entire object lifetime
IoC Containers in .NET
Built-in Microsoft.Extensions.DependencyInjection
ASP.NET Core includes a built-in DI container.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Startup.cs or Program.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Transient - new instance every time
services.AddTransient<IEmailService, EmailService>();
// Scoped - one instance per request
services.AddScoped<IOrderService, OrderService>();
// Singleton - one instance for application lifetime
services.AddSingleton<IConfiguration, Configuration>();
// Register with factory
services.AddTransient<IProductService>(provider =>
{
var repository = provider.GetRequiredService<IProductRepository>();
var cache = provider.GetService<ICache>();
return new ProductService(repository, cache);
});
}
}
|
Service Lifetimes
Transient:
- Created each time they’re requested
- Best for lightweight, stateless services
- Each request gets a new instance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| services.AddTransient<IEmailSender, SmtpEmailSender>();
// Usage
public class Controller
{
private readonly IEmailSender _sender1;
private readonly IEmailSender _sender2;
public Controller(IEmailSender sender1, IEmailSender sender2)
{
_sender1 = sender1; // New instance
_sender2 = sender2; // Another new instance
// sender1 != sender2
}
}
|
Scoped:
- Created once per client request (scope)
- Best for per-request operations
- Same instance within a request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| services.AddScoped<IOrderRepository, OrderRepository>();
// In web request:
// All services within the same HTTP request share the same instance
public class OrderController
{
private readonly IOrderRepository _repo;
public OrderController(IOrderRepository repo)
{
_repo = repo; // Same instance as injected in OrderService
}
}
public class OrderService
{
private readonly IOrderRepository _repo;
public OrderService(IOrderRepository repo)
{
_repo = repo; // Same instance as in OrderController
}
}
|
Singleton:
- Created once and shared throughout application lifetime
- Best for stateless services, configurations, caches
- Must be thread-safe
1
2
3
4
5
6
7
8
9
10
11
12
| services.AddSingleton<ICache, MemoryCache>();
// All requests share the same instance
public class Service1
{
public Service1(ICache cache) { } // Same instance
}
public class Service2
{
public Service2(ICache cache) { } // Same instance
}
|
Lifetime Best Practices
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| public class LifetimeExamples
{
public void ConfigureServices(IServiceCollection services)
{
// GOOD - Stateless service as Transient
services.AddTransient<IEmailValidator, EmailValidator>();
// GOOD - Per-request state as Scoped
services.AddScoped<IShoppingCart, ShoppingCart>();
// GOOD - Shared state as Singleton
services.AddSingleton<IConfigurationProvider, ConfigurationProvider>();
// BAD - Don't inject Scoped into Singleton
// services.AddSingleton<ServiceWithScopedDependency>();
// GOOD - Use IServiceScopeFactory in Singleton
services.AddSingleton<BackgroundService>();
services.AddScoped<IScopedService, ScopedService>();
}
}
public class BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public BackgroundService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task DoWorkAsync()
{
using var scope = _scopeFactory.CreateScope();
var scopedService = scope.ServiceProvider
.GetRequiredService<IScopedService>();
await scopedService.ProcessAsync();
}
}
|
Advanced DI Patterns
1. Factory Pattern with DI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| public interface INotificationFactory
{
INotification CreateNotification(NotificationType type);
}
public class NotificationFactory : INotificationFactory
{
private readonly IServiceProvider _serviceProvider;
public NotificationFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public INotification CreateNotification(NotificationType type)
{
return type switch
{
NotificationType.Email =>
_serviceProvider.GetRequiredService<EmailNotification>(),
NotificationType.Sms =>
_serviceProvider.GetRequiredService<SmsNotification>(),
NotificationType.Push =>
_serviceProvider.GetRequiredService<PushNotification>(),
_ => throw new ArgumentException("Invalid notification type")
};
}
}
// Registration
services.AddTransient<EmailNotification>();
services.AddTransient<SmsNotification>();
services.AddTransient<PushNotification>();
services.AddSingleton<INotificationFactory, NotificationFactory>();
|
2. Decorator Pattern
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| public interface IOrderService
{
Task<Order> CreateOrderAsync(Order order);
}
public class OrderService : IOrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
public async Task<Order> CreateOrderAsync(Order order)
{
return await _repository.SaveAsync(order);
}
}
// Decorator for logging
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 async Task<Order> CreateOrderAsync(Order order)
{
_logger.LogInformation("Creating order {OrderId}", order.Id);
try
{
var result = await _inner.CreateOrderAsync(order);
_logger.LogInformation("Order {OrderId} created successfully", order.Id);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create order {OrderId}", order.Id);
throw;
}
}
}
// Registration with Scrutor
services.AddScoped<IOrderService, OrderService>();
services.Decorate<IOrderService, LoggingOrderService>();
|
3. Named Services
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| public interface IPaymentProcessor
{
Task<PaymentResult> ProcessAsync(Payment payment);
}
public class PayPalProcessor : IPaymentProcessor
{
public async Task<PaymentResult> ProcessAsync(Payment payment)
{
// PayPal implementation
return await Task.FromResult(new PaymentResult());
}
}
public class StripeProcessor : IPaymentProcessor
{
public async Task<PaymentResult> ProcessAsync(Payment payment)
{
// Stripe implementation
return await Task.FromResult(new PaymentResult());
}
}
// Named service resolver
public interface IPaymentProcessorResolver
{
IPaymentProcessor Resolve(string provider);
}
public class PaymentProcessorResolver : IPaymentProcessorResolver
{
private readonly IServiceProvider _serviceProvider;
public PaymentProcessorResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IPaymentProcessor Resolve(string provider)
{
return provider.ToLower() switch
{
"paypal" => _serviceProvider.GetRequiredService<PayPalProcessor>(),
"stripe" => _serviceProvider.GetRequiredService<StripeProcessor>(),
_ => throw new ArgumentException($"Unknown provider: {provider}")
};
}
}
// Registration
services.AddScoped<PayPalProcessor>();
services.AddScoped<StripeProcessor>();
services.AddScoped<IPaymentProcessorResolver, PaymentProcessorResolver>();
|
4. Options Pattern
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| public class EmailSettings
{
public string SmtpServer { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
// appsettings.json
{
"EmailSettings": {
"SmtpServer": "smtp.example.com",
"Port": 587,
"Username": "user@example.com",
"Password": "secret"
}
}
// Startup
services.Configure<EmailSettings>(
Configuration.GetSection("EmailSettings"));
// Service using options
public class EmailService
{
private readonly EmailSettings _settings;
public EmailService(IOptions<EmailSettings> options)
{
_settings = options.Value;
}
public async Task SendAsync(string to, string subject, string body)
{
// Use _settings.SmtpServer, _settings.Port, etc.
}
}
|
Popular IoC Containers
1. Autofac
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| using Autofac;
using Autofac.Extensions.DependencyInjection;
public class Startup
{
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var builder = new ContainerBuilder();
builder.Populate(services);
// Register types
builder.RegisterType<OrderService>()
.As<IOrderService>()
.InstancePerLifetimeScope();
// Register with parameters
builder.RegisterType<ProductService>()
.As<IProductService>()
.WithParameter("timeout", TimeSpan.FromSeconds(30));
// Register modules
builder.RegisterModule<RepositoryModule>();
var container = builder.Build();
return new AutofacServiceProvider(container);
}
}
// Module for organizing registrations
public class RepositoryModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<OrderRepository>()
.As<IOrderRepository>()
.InstancePerLifetimeScope();
builder.RegisterType<ProductRepository>()
.As<IProductRepository>()
.InstancePerLifetimeScope();
}
}
|
2. Ninject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| using Ninject;
public class Program
{
private static IKernel ConfigureServices()
{
var kernel = new StandardKernel();
// Bind interface to implementation
kernel.Bind<IOrderService>().To<OrderService>();
// Bind with scope
kernel.Bind<IOrderRepository>().To<OrderRepository>()
.InRequestScope();
// Bind singleton
kernel.Bind<IConfiguration>().To<Configuration>()
.InSingletonScope();
// Bind with factory
kernel.Bind<IProductService>().ToMethod(ctx =>
{
var repo = ctx.Kernel.Get<IProductRepository>();
return new ProductService(repo);
});
return kernel;
}
public static void Main()
{
var kernel = ConfigureServices();
var service = kernel.Get<IOrderService>();
}
}
|
3. Castle Windsor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| using Castle.MicroKernel.Registration;
using Castle.Windsor;
public class Program
{
private static IWindsorContainer ConfigureServices()
{
var container = new WindsorContainer();
// Register components
container.Register(
Component.For<IOrderService>()
.ImplementedBy<OrderService>()
.LifestyleScoped()
);
container.Register(
Component.For<IProductService>()
.ImplementedBy<ProductService>()
.LifestyleTransient()
);
// Register with interceptors
container.Register(
Component.For<IOrderRepository>()
.ImplementedBy<OrderRepository>()
.Interceptors<LoggingInterceptor>()
);
return container;
}
public static void Main()
{
var container = ConfigureServices();
var service = container.Resolve<IOrderService>();
}
}
|
Testing with DI
Unit Testing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| public class OrderServiceTests
{
[Fact]
public async Task CreateOrder_ValidOrder_ReturnsSuccess()
{
// Arrange
var mockRepository = new Mock<IOrderRepository>();
var mockLogger = new Mock<ILogger<OrderService>>();
mockRepository
.Setup(r => r.SaveAsync(It.IsAny<Order>()))
.ReturnsAsync(new Order { Id = 1 });
var service = new OrderService(
mockRepository.Object,
mockLogger.Object
);
var order = new Order { CustomerId = 1, Total = 100 };
// Act
var result = await service.CreateOrderAsync(order);
// Assert
Assert.NotNull(result);
Assert.Equal(1, result.Id);
mockRepository.Verify(r => r.SaveAsync(order), Times.Once);
}
}
|
Integration Testing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| public class OrderServiceIntegrationTests : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
public OrderServiceIntegrationTests(WebApplicationFactory<Startup> factory)
{
_factory = factory;
}
[Fact]
public async Task CreateOrder_IntegrationTest()
{
// Arrange
var client = _factory.CreateClient();
var order = new Order { CustomerId = 1, Total = 100 };
// Act
var response = await client.PostAsJsonAsync("/api/orders", order);
// Assert
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<Order>();
Assert.NotNull(result);
Assert.True(result.Id > 0);
}
}
|
Common Pitfalls and Best Practices
1. Service Locator Anti-Pattern
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // BAD - Service Locator (anti-pattern)
public class OrderService
{
public void CreateOrder(Order order)
{
var repository = ServiceLocator.GetService<IOrderRepository>();
repository.Save(order);
}
}
// GOOD - Constructor Injection
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
public void CreateOrder(Order order)
{
_repository.Save(order);
}
}
|
2. Captive Dependencies
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // BAD - Singleton capturing Scoped dependency
public class SingletonService
{
private readonly IScopedService _scopedService;
// This keeps the scoped service alive forever!
public SingletonService(IScopedService scopedService)
{
_scopedService = scopedService;
}
}
// GOOD - Use IServiceScopeFactory
public class SingletonService
{
private readonly IServiceScopeFactory _scopeFactory;
public SingletonService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public void DoWork()
{
using var scope = _scopeFactory.CreateScope();
var scopedService = scope.ServiceProvider
.GetRequiredService<IScopedService>();
scopedService.Process();
}
}
|
3. Over-Injection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| // BAD - Too many dependencies
public class OrderService
{
public OrderService(
IOrderRepository repo,
ICustomerService customerService,
IInventoryService inventoryService,
IPaymentService paymentService,
IShippingService shippingService,
INotificationService notificationService,
ILogger logger,
IConfiguration config)
{
// Too many dependencies suggests class does too much
}
}
// GOOD - Extract to smaller services
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IOrderProcessor _processor;
private readonly ILogger<OrderService> _logger;
public OrderService(
IOrderRepository repository,
IOrderProcessor processor,
ILogger<OrderService> logger)
{
_repository = repository;
_processor = processor;
_logger = logger;
}
}
public class OrderProcessor
{
// Handles complex order processing logic
public OrderProcessor(
IPaymentService paymentService,
IInventoryService inventoryService,
INotificationService notificationService)
{
// Focused responsibility
}
}
|
4. Null Checking
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // GOOD - Always validate constructor parameters
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly ILogger<OrderService> _logger;
public OrderService(
IOrderRepository repository,
ILogger<OrderService> logger)
{
_repository = repository
?? throw new ArgumentNullException(nameof(repository));
_logger = logger
?? throw new ArgumentNullException(nameof(logger));
}
}
|
1. Avoid Unnecessary Transient Services
1
2
3
4
5
6
7
| // BAD - Expensive transient service
services.AddTransient<ExpensiveService>();
// GOOD - Use appropriate lifetime
services.AddSingleton<ExpensiveService>();
// or
services.AddScoped<ExpensiveService>();
|
2. Lazy Initialization
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| public class ServiceWithLazyDependency
{
private readonly Lazy<IExpensiveService> _expensiveService;
public ServiceWithLazyDependency(Lazy<IExpensiveService> expensiveService)
{
_expensiveService = expensiveService;
}
public void DoWork()
{
if (SomeCondition())
{
// Service only created if needed
_expensiveService.Value.Process();
}
}
}
// Registration
services.AddTransient<IExpensiveService, ExpensiveService>();
services.AddTransient(provider =>
new Lazy<IExpensiveService>(
() => provider.GetRequiredService<IExpensiveService>()
)
);
|
Conclusion
Dependency Injection and IoC containers are fundamental to modern C# development. Key takeaways include:
- Use constructor injection for required dependencies
- Understand service lifetimes (Transient, Scoped, Singleton)
- Avoid service locator anti-pattern
- Be careful with captive dependencies
- Keep constructors simple and avoid over-injection
- Choose the right IoC container for your needs
- Always validate constructor parameters
- Use factory patterns for complex object creation
- Leverage the Options pattern for configuration
- Write testable code with proper abstractions
By mastering DI and IoC, you can build maintainable, testable, and flexible applications that follow SOLID principles.
References