Dependency Injection in .NET 8

Beginner to Advanced

Overview

Duration: 6-20 hours (can be split across multiple sessions)
Target Audience: .NET developers from beginner to advanced
Prerequisites: Basic C# knowledge, understanding of classes and interfaces

Module 1: Foundations

1.1 What is Dependency Injection?

Learning Objectives:

  • Understand tight coupling problems
  • Learn IoC and DI principles
  • Recognize DI benefits

Theory

  • Problems with tight coupling
  • Inversion of Control (IoC) principle
  • Dependency Injection pattern
  • Benefits: testability, maintainability, flexibility

Hands-on Exercise 1: The Problem

// Before DI - Tightly coupled code
public class OrderService
{
    private EmailService _emailService;
    private PaymentService _paymentService;
    
    public OrderService()
    {
        _emailService = new EmailService(); // Tight coupling
        _paymentService = new PaymentService(); // Hard to test
    }
    
    public void ProcessOrder(Order order)
    {
        _paymentService.ProcessPayment(order.Amount);
        _emailService.SendConfirmation(order.CustomerEmail);
    }
}

Task: Students identify problems with this code and discuss testing challenges.

Hands-on Exercise 2: The Solution (1 hour)

// After DI - Loosely coupled code
public interface IEmailService
{
    void SendConfirmation(string email);
}

public interface IPaymentService
{
    void ProcessPayment(decimal amount);
}

public class OrderService
{
    private readonly IEmailService _emailService;
    private readonly IPaymentService _paymentService;
    
    public OrderService(IEmailService emailService, IPaymentService paymentService)
    {
        _emailService = emailService;
        _paymentService = paymentService;
    }
    
    public void ProcessOrder(Order order)
    {
        _paymentService.ProcessPayment(order.Amount);
        _emailService.SendConfirmation(order.CustomerEmail);
    }
}

Task: Refactor the tight coupling example to use DI.

Module 2: .NET 8 DI Container Basics

2.1 Built-in DI Container

Learning Objectives:

  • Set up DI in .NET 8 applications
  • Understand service registration
  • Learn basic service lifetimes

Theory :

  • .NET 8 built-in container overview
  • Service registration methods
  • Constructor injection basics

Hands-on Exercise 3: Console Application Setup

// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

// Register services
builder.Services.AddTransient<IEmailService, EmailService>();
builder.Services.AddTransient<IPaymentService, PaymentService>();
builder.Services.AddTransient<OrderService>();

var host = builder.Build();

// Use services
var orderService = host.Services.GetRequiredService<OrderService>();
var order = new Order { CustomerEmail = "test@example.com", Amount = 100 };
orderService.ProcessOrder(order);

Task: Create a complete console application using DI.

2.2 Service Lifetimes

Learning Objectives:

  • Understand Transient, Scoped, Singleton
  • Choose appropriate lifetimes
  • Avoid lifetime pitfalls

Theory

  • Transient: New instance every time
  • Scoped: One instance per scope
  • Singleton: One instance for application lifetime

Hands-on Exercise 4: Lifetime Comparison

public interface ICounterService
{
    int GetCount();
}

public class CounterService : ICounterService
{
    private static int _staticCount = 0;
    private int _instanceCount = 0;
    
    public CounterService()
    {
        _staticCount++;
        _instanceCount++;
    }
    
    public int GetCount() => _instanceCount;
}

// Registration variations
builder.Services.AddTransient<ICounterService, CounterService>();
builder.Services.AddScoped<ICounterService, CounterService>();
builder.Services.AddSingleton<ICounterService, CounterService>();

Task: Test different lifetimes and observe behavior differences.

Module 3: Web API with DI

3.1 DI in ASP.NET Core

Learning Objectives:

  • Implement DI in web applications
  • Use DI with controllers
  • Understand scoped lifetime in web context

Hands-on Exercise 5: Web API Project

// Services/IProductService.cs
public interface IProductService
{
    Task<List<Product>> GetAllProductsAsync();
    Task<Product> GetProductByIdAsync(int id);
    Task<Product> CreateProductAsync(Product product);
}

// Controllers/ProductController.cs
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
    private readonly IProductService _productService;
    
    public ProductController(IProductService productService)
    {
        _productService = productService;
    }
    
    [HttpGet]
    public async Task<ActionResult<List<Product>>> GetProducts()
    {
        var products = await _productService.GetAllProductsAsync();
        return Ok(products);
    }
}

// Program.cs
builder.Services.AddScoped<IProductService, ProductService>();

Task: Build a complete Web API with multiple services and controllers.

3.2 Entity Framework Integration

Learning Objectives:

  • Integrate EF Core with DI
  • Understand DbContext lifetime
  • Implement Repository pattern

Hands-on Exercise 6: Database Integration

// Data/ApplicationDbContext.cs
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }
    
    public DbSet<Product> Products { get; set; }
}

// Program.cs
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
    
builder.Services.AddScoped<IProductRepository, ProductRepository>();

Task: Implement a complete data access layer using EF Core and DI.

Module 4: Advanced DI Patterns

4.1 Multiple Implementations

Learning Objectives:

  • Register multiple implementations
  • Use factory patterns
  • Implement strategy pattern with DI

Theory

  • When to use multiple implementations
  • Factory pattern with DI
  • Strategy pattern implementation

Hands-on Exercise 7: Notification System

public interface INotificationService
{
    Task SendNotificationAsync(string message, string recipient);
}

public class EmailNotificationService : INotificationService
{
    public async Task SendNotificationAsync(string message, string recipient)
    {
        // Email implementation
        await Task.Delay(100);
        Console.WriteLine($"Email sent to {recipient}: {message}");
    }
}

public class SmsNotificationService : INotificationService
{
    public async Task SendNotificationAsync(string message, string recipient)
    {
        // SMS implementation
        await Task.Delay(50);
        Console.WriteLine($"SMS sent to {recipient}: {message}");
    }
}

// Registration
builder.Services.AddScoped<EmailNotificationService>();
builder.Services.AddScoped<SmsNotificationService>();
builder.Services.AddScoped<IEnumerable<INotificationService>>(provider =>
    new List<INotificationService>
    {
        provider.GetService<EmailNotificationService>(),
        provider.GetService<SmsNotificationService>()
    });

Task: Implement a notification system that can send via multiple channels.

4.2 Keyed Services (.NET 8 Feature)

Learning Objectives:

  • Use keyed services for multiple implementations
  • Understand when to use keyed services
  • Implement service selection logic

Theory

  • New keyed services feature in .NET 8
  • Benefits over traditional approaches
  • Registration and consumption patterns

Hands-on Exercise 8: Keyed Services Implementation

// Registration with keys
builder.Services.AddKeyedScoped<INotificationService, EmailNotificationService>("email");
builder.Services.AddKeyedScoped<INotificationService, SmsNotificationService>("sms");
builder.Services.AddKeyedScoped<INotificationService, PushNotificationService>("push");

// Usage in services
public class NotificationOrchestrator
{
    private readonly IServiceProvider _serviceProvider;
    
    public NotificationOrchestrator(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public async Task SendNotificationAsync(string type, string message, string recipient)
    {
        var service = _serviceProvider.GetRequiredKeyedService<INotificationService>(type);
        await service.SendNotificationAsync(message, recipient);
    }
}

// Or using [FromKeyedServices] attribute
public class NotificationController : ControllerBase
{
    public async Task<IActionResult> SendEmail(
        [FromKeyedServices("email")] INotificationService emailService,
        NotificationRequest request)
    {
        await emailService.SendNotificationAsync(request.Message, request.Recipient);
        return Ok();
    }
}

Task: Refactor the notification system to use keyed services.

4.3 Decorators and Interceptors

Learning Objectives:

  • Implement decorator pattern with DI
  • Add cross-cutting concerns
  • Understand service interception

Hands-on Exercise 9: Logging Decorator

public class LoggingProductService : IProductService
{
    private readonly IProductService _inner;
    private readonly ILogger<LoggingProductService> _logger;
    
    public LoggingProductService(IProductService inner, ILogger<LoggingProductService> logger)
    {
        _inner = inner;
        _logger = logger;
    }
    
    public async Task<List<Product>> GetAllProductsAsync()
    {
        _logger.LogInformation("Getting all products");
        var result = await _inner.GetAllProductsAsync();
        _logger.LogInformation("Retrieved {Count} products", result.Count);
        return result;
    }
}

// Registration
builder.Services.AddScoped<ProductService>();
builder.Services.AddScoped<IProductService>(provider =>
{
    var productService = provider.GetRequiredService<ProductService>();
    var logger = provider.GetRequiredService<ILogger<LoggingProductService>>();
    return new LoggingProductService(productService, logger);
});

Task: Implement logging and caching decorators.

Module 5: Configuration and Options

5.1 Options Pattern

Learning Objectives:

  • Bind configuration to objects
  • Use IOptions, IOptionsSnapshot, IOptionsMonitor
  • Validate configuration

Theory

  • Options pattern benefits
  • Different option interfaces
  • Configuration validation

Hands-on Exercise 10: Configuration Integration (1.5 hours)

// appsettings.json
{
  "EmailSettings": {
    "SmtpServer": "smtp.example.com",
    "Port": 587,
    "Username": "user@example.com",
    "Password": "password"
  }
}

// EmailSettings.cs
public class EmailSettings
{
    public string SmtpServer { get; set; }
    public int Port { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
}

// EmailService.cs
public class EmailService : IEmailService
{
    private readonly EmailSettings _settings;
    
    public EmailService(IOptions<EmailSettings> options)
    {
        _settings = options.Value;
    }
    
    public async Task SendEmailAsync(string to, string subject, string body)
    {
        // Use _settings to configure SMTP client
    }
}

// Program.cs
builder.Services.Configure<EmailSettings>(
    builder.Configuration.GetSection("EmailSettings"));
builder.Services.AddScoped<IEmailService, EmailService>();

Task: Implement configuration-driven services with validation.

Module 6: Testing with DI

6.1 Unit Testing

Learning Objectives:

  • Mock dependencies for unit tests
  • Use dependency injection in tests
  • Test service registration

Theory

  • Benefits of DI for testing
  • Mocking frameworks overview
  • Test-driven development with DI

Hands-on Exercise 11: Unit Testing

// Test class
public class OrderServiceTests
{
    private readonly Mock<IEmailService> _mockEmailService;
    private readonly Mock<IPaymentService> _mockPaymentService;
    private readonly OrderService _orderService;
    
    public OrderServiceTests()
    {
        _mockEmailService = new Mock<IEmailService>();
        _mockPaymentService = new Mock<IPaymentService>();
        _orderService = new OrderService(_mockEmailService.Object, _mockPaymentService.Object);
    }
    
    [Fact]
    public async Task ProcessOrder_ShouldCallPaymentService()
    {
        // Arrange
        var order = new Order { Amount = 100, CustomerEmail = "test@example.com" };
        
        // Act
        await _orderService.ProcessOrder(order);
        
        // Assert
        _mockPaymentService.Verify(x => x.ProcessPayment(100), Times.Once);
        _mockEmailService.Verify(x => x.SendConfirmation("test@example.com"), Times.Once);
    }
}

Task: Write comprehensive unit tests for the services created in previous exercises.

6.2 Integration Testing

Learning Objectives:

  • Test with real DI container
  • Use WebApplicationFactory
  • Test service lifetimes

Hands-on Exercise 12: Integration Testing

public class ProductControllerIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly HttpClient _client;
    
    public ProductControllerIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Replace services for testing
                services.AddScoped<IProductService, TestProductService>();
            });
        }).CreateClient();
    }
    
    [Fact]
    public async Task GetProducts_ReturnsSuccessStatusCode()
    {
        var response = await _client.GetAsync("/api/products");
        response.EnsureSuccessStatusCode();
    }
}

Task: Create integration tests for the Web API.

Module 7: Performance and Best Practices

7.1 Performance Optimization

Learning Objectives:

  • Understand DI performance implications
  • Optimize service registration
  • Monitor container performance

Theory

  • .NET 8 performance improvements
  • Container resolution costs
  • Best practices for performance

Hands-on Exercise 13: Performance Benchmarking

[MemoryDiagnoser]
public class DIPerformanceBenchmark
{
    private ServiceProvider _serviceProvider;
    
    [GlobalSetup]
    public void Setup()
    {
        var services = new ServiceCollection();
        services.AddTransient<ITransientService, TransientService>();
        services.AddScoped<IScopedService, ScopedService>();
        services.AddSingleton<ISingletonService, SingletonService>();
        _serviceProvider = services.BuildServiceProvider();
    }
    
    [Benchmark]
    public ITransientService GetTransientService()
    {
        return _serviceProvider.GetRequiredService<ITransientService>();
    }
    
    [Benchmark]
    public ISingletonService GetSingletonService()
    {
        return _serviceProvider.GetRequiredService<ISingletonService>();
    }
}

Task: Benchmark different service lifetimes and registration patterns.

7.2 Best Practices and Common Pitfalls

Learning Objectives:

  • Avoid common DI mistakes
  • Follow best practices
  • Understand captive dependencies

Theory

  • Common anti-patterns
  • Captive dependency problem
  • Service locator anti-pattern
  • Best practices checklist

Hands-on Exercise 14: Identifying Problems

// Anti-pattern examples to fix
public class BadService
{
    private readonly IServiceProvider _serviceProvider; // Service locator
    
    public BadService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public void DoWork()
    {
        var service = _serviceProvider.GetService<IAnotherService>(); // Bad!
        service.DoSomething();
    }
}

// Captive dependency example
public class SingletonService
{
    private readonly IScopedService _scopedService; // Problem!
    
    public SingletonService(IScopedService scopedService)
    {
        _scopedService = scopedService;
    }
}

Task: Identify and fix anti-patterns in provided code examples.

Module 8: Advanced Scenarios

8.1 Custom DI Container Integration

Learning Objectives:

  • Integrate third-party containers
  • Compare container features
  • Migration strategies

Theory

  • When to use third-party containers
  • Popular alternatives (Autofac, Ninject)
  • Migration considerations

Hands-on Exercise 15: Autofac Integration

// Program.cs with Autofac
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(containerBuilder =>
{
    containerBuilder.RegisterType<ProductService>().As<IProductService>().InstancePerLifetimeScope();
    containerBuilder.RegisterModule<AutofacModule>();
});

// AutofacModule.cs
public class AutofacModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<EmailService>().As<IEmailService>().SingleInstance();
        builder.RegisterDecorator<LoggingEmailService, IEmailService>();
    }
}

Task: Migrate a portion of the application to use Autofac.

8.2 Background Services and Hosted Services

Learning Objectives:

  • Use DI with background services
  • Implement hosted services
  • Handle scoped services in singletons

Hands-on Exercise 16: Background Processing

public class DataProcessingService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<DataProcessingService> _logger;
    
    public DataProcessingService(IServiceProvider serviceProvider, ILogger<DataProcessingService> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = _serviceProvider.CreateScope();
            var dataProcessor = scope.ServiceProvider.GetRequiredService<IDataProcessor>();
            
            await dataProcessor.ProcessDataAsync();
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

Task: Implement a background service that processes data using scoped services.

Module 9: Microservices and DI

9.1 DI in Microservices Architecture

Learning Objectives:

  • Design DI for microservices
  • Handle service discovery
  • Implement resilience patterns

Theory

  • Microservices DI challenges
  • Service discovery patterns
  • Circuit breaker and retry patterns

Hands-on Exercise 17: HTTP Client Factory

// Program.cs
builder.Services.AddHttpClient<IOrderService, OrderService>(client =>
{
    client.BaseAddress = new Uri("https://api.orders.com");
    client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
});

builder.Services.AddHttpClient<IInventoryService, InventoryService>()
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy());

// Service implementation
public class OrderService : IOrderService
{
    private readonly HttpClient _httpClient;
    
    public OrderService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    
    public async Task<Order> GetOrderAsync(int orderId)
    {
        var response = await _httpClient.GetAsync($"/orders/{orderId}");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<Order>();
    }
}

Task: Implement service-to-service communication with resilience patterns.

Module 10: Real-World Project (2 hours)Final Project: E-commerce System

Learning Objectives:

  • Apply all learned concepts
  • Build a complete system
  • Demonstrate best practices

Project Requirements:

  • Product catalog service
  • Order processing service
  • User management service
  • Payment integration
  • Email notifications
  • Background order processing
  • Comprehensive testing
  • Performance monitoring

Project Structure:

ECommerceSystem/
├── src/
│   ├── ECommerce.API/
│   ├── ECommerce.Core/
│   ├── ECommerce.Infrastructure/
│   └── ECommerce.Services/
├── tests/
│   ├── ECommerce.UnitTests/
│   └── ECommerce.IntegrationTests/
└── docker-compose.yml

Task: Students build a complete e-commerce system applying all DI patterns learned.

Assessment and Certification

Knowledge Check Questions (Throughout Course)

  1. What are the three main service lifetimes in .NET DI?
  2. How do you register multiple implementations of the same interface?
  3. What is the captive dependency problem?
  4. When should you use keyed services?
  5. How do you properly test services with dependencies?

Practical Assessments

  1. Beginner: Convert tightly coupled code to use DI
  2. Intermediate: Implement a multi-service Web API with proper lifetime management
  3. Advanced: Design a microservices system with DI best practices

Final Project Evaluation Criteria

  • Architecture: Proper separation of concerns
  • DI Implementation: Correct service registration and lifetime management
  • Testing: Comprehensive unit and integration tests
  • Performance: Optimized service resolution
  • Best Practices: Following established patterns and avoiding anti-patterns

Additional Resources

Documentation

Tools and Libraries

  • Autofac: Advanced DI container
  • Scrutor: Assembly scanning for service registration
  • BenchmarkDotNet: Performance testing
  • Moq: Mocking framework for testing

Upon completing this , you will:

  • Understand DI principles and benefits
  • Master .NET 8 DI container features
  • Implement complex DI scenarios
  • Write testable, maintainable code
  • Apply DI best practices in real-world projects
  • Avoid common pitfalls and anti-patterns

Leave a Reply