Google Anlatics

Wednesday, February 7, 2024

Mastering Dependency Injection in C# and ASP.NET Web API: A Comprehensive Guide

Hey there! 👋 Let's dive into Dependency Injection (DI) in a way that actually makes sense. No buzzwords, just practical knowledge you can use today.

Why Should You Care About DI?

Before we dive into the code, let's talk about why DI matters in real-world development:

  • Ever inherited a codebase where changing one thing breaks everything else?
  • Spent hours mocking dependencies for unit tests?
  • Struggled to swap implementations for different environments?

DI helps solve these headaches. Let's see how.

Starting Simple: The Basics

Let's build this up step by step with a real-world scenario. Imagine you're building an e-commerce API:

// Before DI - The Painful Way 😫 public class OrderController { private readonly OrderService _orderService = new OrderService( new PaymentProcessor( new PaymentGateway() ) ); } // After DI - The Clean Way 😎 public class OrderController { private readonly IOrderService _orderService; public OrderController(IOrderService orderService) { _orderService = orderService; } }

Real-World DI Patterns

The Constructor Injection Pattern (Your Go-To Choice)

This is what you'll use 90% of the time:


public class OrderProcessor { private readonly IPaymentGateway _paymentGateway; private readonly IOrderRepository _orderRepository; private readonly ILogger<OrderProcessor> _logger; public OrderProcessor( IPaymentGateway paymentGateway, IOrderRepository orderRepository, ILogger<OrderProcessor> logger) { _paymentGateway = paymentGateway; _orderRepository = orderRepository; _logger = logger; } public async Task ProcessOrder(Order order) { _logger.LogInformation($"Processing order {order.Id}"); // Your business logic here } }

Service Lifetimes Explained Simply

Think of services like employees in a store:

public void ConfigureServices(IServiceCollection services) { // The Store Manager (Singleton) - One person, there all day services.AddSingleton<IStoreConfig, StoreConfig>(); // The Cashier (Scoped) - One per customer services.AddScoped<IOrderProcessor, OrderProcessor>(); // The Shopping Assistant (Transient) - New one for each task services.AddTransient<IProductValidator, ProductValidator>(); }

Testing Made Easy

Here's how DI makes testing a breeze:

public class OrderProcessorTests { [Fact] public async Task ProcessOrder_WithValidPayment_Succeeds() { // Arrange var mockPaymentGateway = new Mock<IPaymentGateway>(); mockPaymentGateway.Setup(x => x.ProcessPayment(It.IsAny<decimal>())) .ReturnsAsync(true); var processor = new OrderProcessor( mockPaymentGateway.Object, Mock.Of<IOrderRepository>(), Mock.Of<ILogger<OrderProcessor>>() ); // Act await processor.ProcessOrder(new Order { Amount = 100m }); // Assert mockPaymentGateway.Verify(x => x.ProcessPayment(100m), Times.Once); } }

Advanced Patterns (When You Need Them)

The Decorator Pattern (Adding Behavior)

Perfect for adding caching, logging, or validation:

public class CachingOrderService : IOrderService { private readonly IOrderService _inner; private readonly ICache _cache; public CachingOrderService(IOrderService inner, ICache cache) { _inner = inner; _cache = cache; } public async Task<Order> GetOrder(int id) { var cacheKey = $"order_{id}"; if (_cache.TryGet(cacheKey, out Order order)) return order; order = await _inner.GetOrder(id); _cache.Set(cacheKey, order); return order; } }

Factory Pattern (When You Need Dynamic Creation)


public class PaymentProcessorFactory { private readonly IServiceProvider _services; public PaymentProcessorFactory(IServiceProvider services) { _services = services; } public IPaymentProcessor Create(PaymentMethod method) { return method switch { PaymentMethod.CreditCard => _services.GetRequiredService<ICreditCardProcessor>(), PaymentMethod.PayPal => _services.GetRequiredService<IPayPalProcessor>(), _ => throw new NotSupportedException($"Payment method {method} not supported") }; } }

Common Pitfalls (Learn From Our Mistakes)

❌ Circular Dependencies


// Don't do this! public class ServiceA { public ServiceA(ServiceB b) { } } public class ServiceB { public ServiceB(ServiceA a) { } } // Do this instead public class ServiceA { private readonly IServiceProvider _services; private ServiceB _b; public ServiceA(IServiceProvider services) { _services = services; } private ServiceB B => _b ??= _services.GetRequiredService<ServiceB>(); }

❌ Disposing Services Incorrectly

// Remember to dispose scoped services! public async Task ProcessOrders() { using var scope = _services.CreateScope(); var processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>(); await processor.ProcessPendingOrders(); }

Pro Tips 🚀

  1. Keep services focused (Single Responsibility Principle)
  2. Use constructor injection by default
  3. Register services in feature modules for better organization
  4. Always dispose of scoped services
  5. Use middleware for cross-cutting concerns

Wrapping Up

DI isn't just a pattern – it's a tool that makes your life easier. Start with constructor injection, use scoped services wisely, and remember: if a class needs to create its dependencies, it's probably doing too much.

No comments:

Sri Lanka .NET 
                Forum Member