NET Centric ComputingUnit 88 min read
Dependency Injection (DI) & IoC Containers: Core Concepts, Lifecycle, and Real-World Use
Unit 8 of NET Centric Computing covers Dependency Injection (DI) principles, Inversion of Control (IoC) containers, service lifetimes (transient, scoped, singleton), registration patterns, and how DI solves tight coupling in ASP.NET Core. Includes real-world examples from eSewa, Ncell, and WhatsApp, plus a worked examp
What is Dependency Injection (DI)?
Dependency Injection is a design pattern that removes hard-coded dependencies and makes an application more modular, testable, and maintainable. Instead of a class creating its own dependencies, an external entity (the IoC container) provides them. This follows the Dependency Inversion Principle (DIP) from SOLID principles.
Why Use DI?
- Decouples components: Classes don’t need to know how to create their dependencies.
- Easier testing: Dependencies can be mocked or replaced.
- Reusability: Components can be reused in different contexts.
- Maintainability: Changes in one part don’t break others.
classDiagram
class OrderProcessor {
+ProcessOrder()
}
class PaymentService {
+Charge()
}
class EmailService {
+SendConfirmation()
}
OrderProcessor --> PaymentService : "uses"
OrderProcessor --> EmailService : "uses"
note for OrderProcessor "Dependencies are injected, not created internally"Inversion of Control (IoC) Containers
An IoC container is the engine that manages object creation, lifetime, and dependency resolution. ASP.NET Core’s built-in DI container is an IoC container. It:
- Registers services (e.g.,
services.AddTransient<IPaymentService, PaymentService>()). - Resolves dependencies when requested (e.g., via constructor injection).
- Manages lifetimes (transient, scoped, singleton).
How IoC Works in ASP.NET Core
- Registration: Services are registered in
Program.csorStartup.cs. - Resolution: The container creates and injects dependencies when a class is instantiated.
- Disposal: The container handles cleanup (e.g.,
IDisposableobjects).
// Example: Registering a service in Program.cs
builder.Services.AddTransient<IPaymentService, PaymentService>();
builder.Services.AddScoped<IEmailService, EmailService>();
Service Lifetimes in DI
The IoC container manages three types of service lifetimes:
| Lifetime Type | Scope | Example Use Case | When to Use |
|---|---|---|---|
| Transient | New instance per request | IEmailService (stateless operations) |
Stateless services (e.g., logging) |
| Scoped | One instance per HTTP request | IOrderRepository (shared per request) |
Database contexts, request-specific data |
| Singleton | One instance for the app | IConfiguration (global settings) |
Caching, configuration, app-wide services |
Real-World Applications of DI
1. eSewa (Nepal)
- Use Case: Payment processing and user authentication.
- DI in Action:
IPaymentGatewayis injected intoOrderService(transient lifetime).IUserRepositoryis scoped to ensure data consistency per transaction.- Why? eSewa can swap payment providers (e.g., from Khalti to IME Pay) without changing
OrderService.
2. WhatsApp (Global)
- Use Case: Message routing and encryption.
- DI in Action:
IMessageEncryptoris injected intoMessageHandler(singleton for performance).IUserSessionis scoped per chat session.- Why? WhatsApp can update encryption libraries without breaking message handling.
3. Ncell (Nepal)
- Use Case: Billing and customer service.
- DI in Action:
IBillingServiceis registered as scoped to avoid duplicate charges.ILoggeris transient for thread safety.- Why? Ncell can scale billing services independently.
Worked Example: Loan Approval System
Scenario: A bank’s loan approval system uses DI to separate validation, credit scoring, and notification logic.
Step 1: Define Interfaces and Classes
public interface ICreditScorer { bool IsEligible(Customer customer); }
public interface ILoanValidator { bool Validate(Customer customer); }
public interface INotificationService { void SendApprovalEmail(Customer customer); }
public class LoanService {
private readonly ICreditScorer _scorer;
private readonly ILoanValidator _validator;
private readonly INotificationService _notifier;
// Dependencies are injected via constructor
public LoanService(ICreditScorer scorer, ILoanValidator validator, INotificationService notifier) {
_scorer = scorer;
_validator = validator;
_notifier = notifier;
}
public void ApproveLoan(Customer customer) {
if (_validator.Validate(customer) && _scorer.IsEligible(customer)) {
_notifier.SendApprovalEmail(customer);
}
}
}
Step 2: Register Services in Program.cs
builder.Services.AddTransient<ICreditScorer, CreditScorer>();
builder.Services.AddScoped<ILoanValidator, LoanValidator>();
builder.Services.AddSingleton<INotificationService, EmailNotificationService>();
Step 3: Resolve Dependencies
The IoC container automatically injects the correct implementations when LoanService is created:
var loanService = app.Services.GetRequiredService<LoanService>();
loanService.ApproveLoan(new Customer { Name = "Ramesh", CreditScore = 750 });
DI vs. Traditional Coupling
| Traditional Approach (Tight Coupling) | Dependency Injection (Loose Coupling) |
|---|---|
| Class creates its own dependencies. | Dependencies are injected externally. |
| Hard to test (mocking is difficult). | Easy to mock dependencies for unit tests. |
| Changes in one class break others. | Changes are isolated. |
Example: PaymentService creates EmailService. |
OrderProcessor receives IPaymentService and IEmailService. |
Common Pitfalls and Best Practices
❌ Anti-Patterns
- Overusing Singleton: Can lead to shared state issues (e.g., caching stale data).
- Circular Dependencies: Class A depends on B, which depends on A.
- Ignoring Disposal: Forgetting to implement
IDisposablefor scoped services.
✅ Best Practices
- Prefer Interfaces: Always inject interfaces, not concrete classes.
- Use Constructor Injection: More explicit than property injection.
- Avoid Service Locator: Directly resolving dependencies in classes violates DI.
- Name Services Clearly: Use suffixes like
Service,Repository, orManager.
The DI Container Lifecycle
Exam Tip
What Examiners Look For:
- Definitions:
- Clearly distinguish between DI (pattern) and IoC (container).
- Explain transient, scoped, and singleton lifetimes with examples.
- Code Examples:
- Show registration (
AddTransient,AddScoped,AddSingleton). - Demonstrate constructor injection in a class.
- Show registration (
- Real-World Scenarios:
- Relate DI to eSewa’s payment system, WhatsApp’s message routing, or Ncell’s billing.
- Explain how DI enables mocking in unit tests (e.g., replacing
IEmailServicewith a fake).
- Common Mistakes:
- Avoid vague answers like “DI improves maintainability.” Instead, say:
“DI decouples
OrderServicefromPaymentGateway, allowing Ncell to switch providers without modifyingOrderService.”
- Avoid vague answers like “DI improves maintainability.” Instead, say:
“DI decouples
- Diagrams:
- Draw a class diagram showing interfaces and implementations.
- Sketch the IoC container lifecycle (register → resolve → dispose).
Past Exam Questions Answered:
- Life Cycle of DI Container:
- Registration → Resolution → Disposal (see lifecycle diagram above).
- SQL Injection Prevention:
- Use parameterized queries (not string concatenation) and Entity Framework Core (which escapes inputs by default).
- Example:
// Vulnerable (SQL Injection risk) var query = $"SELECT * FROM Users WHERE Username = '{username}'"; // Safe (Parameterized Query) var user = await _context.Users.FirstOrDefaultAsync(u => u.Username == username);
- Types of Services Managed by IoC:
- Transient: New instance every time (e.g.,
ILogger). - Scoped: One instance per request (e.g.,
DbContext). - Singleton: One instance for the app (e.g.,
IConfiguration).
- Transient: New instance every time (e.g.,
Based on the TU BSc CSIT syllabus for NET Centric Computing (CSC367), unit 8.
Discussion
Loading…