Section 1 of 6

What is Dependency Injection?

🎯 What You'll Learn

  • What Dependency Injection (DI) is and why it matters
  • The problem DI solves
  • Understanding dependencies and coupling
  • The Dependency Inversion Principle (SOLID)
  • Benefits of using DI
  • DI vs Service Locator pattern
  • How DI fits into InvenTrack

The Problem: Tight Coupling

Let's start with a problem. Imagine you're building InvenTrack and need to send email notifications when inventory is low:

❌ Tightly Coupled Code C#
public class InventoryService
{
    public void CheckStock(Product product)
    {
        if (product.QuantityInStock < 10)
        {
            // Creating dependency directly - TIGHT COUPLING!
            var emailService = new EmailService();
            emailService.SendLowStockAlert(product.Name);
        }
    }
}

Problems with This Approach

  • Hard to Test: Can't test without sending real emails
  • Hard to Change: Want to use a different email provider? Modify every class
  • Hard to Reuse: InventoryService is tied to EmailService
  • Violates SOLID: Depends on concrete implementation, not abstraction

The Solution: Dependency Injection

Dependency Injection is a design pattern where objects receive their dependencies from external sources rather than creating them.

βœ… Loosely Coupled Code C#
// 1. Define an interface (abstraction)
public interface IEmailService
{
    void SendLowStockAlert(string productName);
}

// 2. Implement the interface
public class EmailService : IEmailService
{
    public void SendLowStockAlert(string productName)
    {
        // Send actual email
        Console.WriteLine($"Sending low stock alert for {productName}");
    }
}

// 3. Inject the dependency via constructor
public class InventoryService
{
    private readonly IEmailService _emailService;

    // Dependency is INJECTED, not created
    public InventoryService(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public void CheckStock(Product product)
    {
        if (product.QuantityInStock < 10)
        {
            _emailService.SendLowStockAlert(product.Name);
        }
    }
}
πŸ’‘ Key Concept

InventoryService now depends on the abstraction (IEmailService), not the concrete implementation (EmailService). This is the Dependency Inversion Principle from SOLID.

Benefits of This Approach

  • Easy to Test: Inject a mock IEmailService in tests
  • Easy to Change: Swap implementations without changing InventoryService
  • Reusable: InventoryService works with any IEmailService
  • Follows SOLID: Depends on abstractions, not concretions

What is a Dependency?

A dependency is any object that another object needs to function.

Example Dependencies C#
public class ProductService
{
    // These are ALL dependencies:
    private readonly IProductRepository _repository;      // Database access
    private readonly ILogger<ProductService> _logger;  // Logging
    private readonly IEmailService _emailService;          // Email
    private readonly ICacheService _cacheService;          // Caching

    public ProductService(
        IProductRepository repository,
        ILogger<ProductService> logger,
        IEmailService emailService,
        ICacheService cacheService)
    {
        _repository = repository;
        _logger = logger;
        _emailService = emailService;
        _cacheService = cacheService;
    }
}

The Dependency Inversion Principle

The D in SOLID stands for Dependency Inversion Principle:

πŸ“– Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Abstractions should not depend on details. Details should depend on abstractions.

Before DIP (Bad)

Violates DIP Text
InventoryService β†’ EmailService (concrete class)
    ↓
Tightly coupled to implementation

After DIP (Good)

Follows DIP Text
InventoryService β†’ IEmailService (interface)
                         ↑
                    EmailService (implements interface)
    ↓
Both depend on abstraction

Types of Dependency Injection

1. Constructor Injection (Recommended)

Constructor Injection C#
public class ProductService
{
    private readonly IProductRepository _repository;

    public ProductService(IProductRepository repository)
    {
        _repository = repository;
    }
}

βœ… Preferred: Dependencies are clear, immutable, and required

2. Property Injection (Rare)

Property Injection C#
public class ProductService
{
    public IProductRepository Repository { get; set; }
}

⚠️ Avoid: Dependencies are optional and mutable

3. Method Injection (Specific Cases)

Method Injection C#
public class ProductService
{
    public void ProcessProduct(Product product, ILogger logger)
    {
        logger.LogInformation($"Processing {product.Name}");
    }
}

⚠️ Rare: Only when dependency varies per method call

πŸ’‘ Best Practice

Use Constructor Injection 99% of the time. It makes dependencies explicit and ensures objects are always in a valid state.

Benefits of Dependency Injection

1. Testability

Unit Test Example C#
[Fact]
public void CheckStock_WhenLow_SendsEmail()
{
    // Arrange - use a MOCK email service
    var mockEmailService = new Mock<IEmailService>();
    var service = new InventoryService(mockEmailService.Object);
    
    var product = new Product { Name = "Laptop", QuantityInStock = 5 };

    // Act
    service.CheckStock(product);

    // Assert - verify email was sent
    mockEmailService.Verify(x => x.SendLowStockAlert("Laptop"), Times.Once);
}

2. Flexibility

Easily swap implementations:

Multiple Implementations C#
// Development: Log emails to console
public class ConsoleEmailService : IEmailService
{
    public void SendLowStockAlert(string productName)
    {
        Console.WriteLine($"[DEV] Low stock: {productName}");
    }
}

// Production: Send real emails via SendGrid
public class SendGridEmailService : IEmailService
{
    public void SendLowStockAlert(string productName)
    {
        // Send via SendGrid API
    }
}

3. Maintainability

Changes to EmailService don't affect InventoryService as long as the interface stays the same.

4. Separation of Concerns

Each class has a single responsibility. InventoryService manages inventory, EmailService sends emails.

DI Container (IoC Container)

A DI Container (also called an IoC Container - Inversion of Control) is a framework that manages object creation and dependency resolution.

Manual DI (Without Container) C#
// You have to manually create everything
var emailService = new EmailService();
var inventoryService = new InventoryService(emailService);
var controller = new ProductsController(inventoryService);
With DI Container (ASP.NET Core) C#
// Register services once in Program.cs
builder.Services.AddScoped<IEmailService, EmailService>();
builder.Services.AddScoped<IInventoryService, InventoryService>();

// Container automatically creates and injects dependencies!
// ProductsController gets InventoryService, which gets EmailService
πŸ’‘ How It Works

When ASP.NET Core needs a ProductsController, the DI container:
1. Sees it needs IInventoryService
2. Creates InventoryService
3. Sees InventoryService needs IEmailService
4. Creates EmailService
5. Injects EmailService into InventoryService
6. Injects InventoryService into ProductsController

Key Takeaways

  • Dependency Injection is a design pattern for loose coupling
  • Objects receive dependencies instead of creating them
  • Depend on abstractions (interfaces), not concrete classes
  • Dependency Inversion Principle: Both high and low-level modules depend on abstractions
  • Constructor Injection is the preferred method
  • Benefits: Testability, flexibility, maintainability, separation of concerns
  • A DI Container manages object creation and dependency resolution
  • ASP.NET Core has a built-in DI container
  • Register services in Program.cs, inject via constructor
🎯 Next Steps

You now understand what Dependency Injection is and why it's essential! In the next section, we'll explore The Built-in DI Container in ASP.NET Coreβ€”how to register services, resolve dependencies, and use the container effectively.