Dependency Injection & IoC Principles

10 questions found

Dependency injection means a class RECEIVES its dependencies (typically via constructor parameters) from an external source, rather than constructing them itself with 'new' — this decouples the class from specific implementations, making it far easier to swap implementations (like for testing) and centralizing how objects get wired together.
// Without DI: tightly coupled, hard to test
public class OrderService {
  private readonly SqlDatabase _db = new SqlDatabase(); // hardcoded dependency
}

// With DI: loosely coupled, testable
public class OrderService {
  private readonly IDatabase _db;
  public OrderService(IDatabase db) { _db = db; } // injected
}
Real-world example Making a service class testable by injecting a fake/mock database implementation instead of a real SQL connection during unit tests.

Common follow-ups: What are the three common forms of dependency injection: constructor, property, and method injection?

Interfaces & Abstract Classes
Register a service's interface-to-implementation mapping in Program.cs using builder.Services.AddXxx(), then ASP.NET Core automatically INJECTS it into any controller or class constructor that declares a dependency on that interface — you never call 'new' or manually resolve it yourself.
// Program.cs
builder.Services.AddScoped<IOrderService, OrderService>();

// Automatically injected into a controller's constructor
public class OrdersController : ControllerBase {
  private readonly IOrderService _orderService;
  public OrdersController(IOrderService orderService) { _orderService = orderService; }
}
Real-world example Registering a repository or service interface in an ASP.NET Core web API's startup configuration.

Common follow-ups: What happens if you inject a service into a constructor but forget to register it in the DI container?

Interfaces & Abstract Classes
Singleton creates ONE instance for the entire application's lifetime, shared across all requests; Scoped creates ONE instance PER REQUEST (or per logical scope), shared within that single request but recreated for the next; Transient creates a BRAND NEW instance every single time the service is requested/injected, even multiple times within the same request.
builder.Services.AddSingleton<ICacheService, CacheService>();   // one instance, app-wide
builder.Services.AddScoped<IOrderService, OrderService>();       // one instance per HTTP request
builder.Services.AddTransient<IEmailBuilder, EmailBuilder>();    // new instance every injection
Real-world example Registering a shared in-memory cache as Singleton, a per-request database context as Scoped, and a lightweight stateless helper as Transient.

Common follow-ups: What bug (a 'captive dependency') occurs if a Singleton service depends on a Scoped service?

Fundamentals
IoC is the broader principle that a component should NOT control the creation/lifecycle of its own dependencies — control is 'inverted' to an external source (a framework, container, or calling code). Dependency injection is ONE common technique for achieving IoC (others include the Service Locator pattern or event-driven/callback-based inversion), specifically by passing dependencies in rather than the component reaching out to fetch them.
// IoC in action: the FRAMEWORK controls when/how OrderService is created and injected,
// not OrderService's own code or its direct caller
public class OrdersController {
  public OrdersController(IOrderService orderService) { /* framework provides this */ }
}
Real-world example Understanding DI as a specific implementation of the broader IoC principle, distinguishing it from other IoC techniques like the Service Locator pattern.

Common follow-ups: Why is the Service Locator pattern generally considered an inferior alternative to constructor-based DI?

Design Patterns in C#
Constructor injection passes dependencies through the constructor, guaranteeing an object is NEVER in a partially-initialized state and making dependencies clearly visible in the class's public API; property injection sets dependencies via public properties AFTER construction, which can leave an object usable-but-broken if a required property is forgotten, and hides the true dependency requirements from the constructor signature.
// Constructor injection: dependency is guaranteed present and visible
public class OrderService {
  private readonly ILogger _logger;
  public OrderService(ILogger logger) { _logger = logger; } // impossible to construct without it
}

// Property injection: risky, can be forgotten
public class OrderServiceRisky {
  public ILogger? Logger { get; set; } // might be null if forgotten!
}
Real-world example Preferring constructor injection for REQUIRED dependencies, reserving property injection (if used at all) for genuinely OPTIONAL ones.

Common follow-ups: In what specific, narrower scenarios might property injection still be a reasonable choice?

Fundamentals
A captive dependency occurs when a longer-lived service (like Singleton) injects a shorter-lived one (like Scoped) — the Scoped instance gets 'captured' and effectively lives as long as the Singleton, defeating its intended per-request lifecycle and potentially causing stale data or thread-safety issues; ASP.NET Core's DI container detects this specific misconfiguration and throws an exception at service resolution time by default (with scope validation enabled).
builder.Services.AddSingleton<ICacheService, CacheService>();
builder.Services.AddScoped<IDbContext, AppDbContext>();

public class CacheService : ICacheService {
  public CacheService(IDbContext db) { } // Error at startup: captive dependency detected!
}
Real-world example Debugging a subtle production bug where a database context was unexpectedly shared and stale across multiple unrelated requests.

Common follow-ups: How would you correctly restructure a Singleton that genuinely needs access to Scoped services on demand, using IServiceScopeFactory?

Memory & Garbage Collection
Inject a factory delegate (Func<string, IService>) or a dedicated factory interface/class instead of the service directly — the factory itself is registered normally in DI, and its implementation resolves the correct concrete service (often via a keyed lookup among several registered implementations) at the moment it's actually needed, based on runtime information the container itself doesn't have.
public interface IPaymentProcessorFactory { IPaymentProcessor Create(string provider); }

public class PaymentProcessorFactory : IPaymentProcessorFactory {
  private readonly IEnumerable<IPaymentProcessor> _processors;
  public PaymentProcessorFactory(IEnumerable<IPaymentProcessor> processors) { _processors = processors; }
  public IPaymentProcessor Create(string provider) =>
    _processors.First(p => p.ProviderName == provider);
}
Real-world example Selecting between multiple registered payment gateway implementations (Stripe, PayPal, etc.) at runtime based on a user's chosen provider.

Common follow-ups: How does .NET 8's newer KEYED SERVICES feature (AddKeyedSingleton, etc.) simplify this exact pattern natively?

Design Patterns in C#
Inject IServiceScopeFactory (which is itself safely Singleton-lifetime) into the Singleton, and CREATE A NEW SCOPE explicitly each time you need to use a Scoped service — resolving the Scoped dependency from within that fresh, short-lived scope (and disposing it afterward) rather than injecting it directly into the Singleton's constructor.
public class BackgroundWorker : IHostedService {
  private readonly IServiceScopeFactory _scopeFactory;
  public BackgroundWorker(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; }

  public async Task DoWorkAsync() {
    using var scope = _scopeFactory.CreateScope(); // fresh scope, correctly short-lived
    var db = scope.ServiceProvider.GetRequiredService<IDbContext>();
    await db.SaveChangesAsync();
  } // scope (and the Scoped service within it) disposed here
}
Real-world example Correctly accessing a Scoped Entity Framework DbContext from within a long-lived background/hosted service in ASP.NET Core.

Common follow-ups: Why must the scope specifically be created and disposed PER OPERATION, rather than once at the Singleton's construction time?

Asynchronous Programming
Implement IServiceProviderFactory<TContainerBuilder> to swap the built-in container for a third-party one, then use ConfigureContainer() in the host builder to register services using that container's richer API — this lets you keep using standard ASP.NET Core hosting/middleware while gaining advanced DI capabilities the built-in container intentionally omits for simplicity.
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(containerBuilder => {
  containerBuilder.RegisterAssemblyTypes(typeof(Program).Assembly)
    .Where(t => t.Name.EndsWith("Service"))
    .AsImplementedInterfaces(); // convention-based registration, not built into the default container
});
Real-world example Adopting Autofac in an ASP.NET Core project specifically to get convention-based auto-registration or property injection support.

Common follow-ups: What are the trade-offs (startup performance, complexity) of swapping in a third-party DI container versus the built-in one?

Design Patterns in C#
The built-in ASP.NET Core container requires exactly ONE constructor be usable for injection by default (multiple public constructors typically cause an ambiguity exception, UNLESS the container can determine a single 'greediest' match where all parameters are resolvable) — best practice is to expose a SINGLE public constructor for DI-managed classes, keeping any convenience overloads private or removing them entirely to avoid ambiguity.
public class OrderService {
  // Having two public constructors like this can cause DI resolution ambiguity or errors:
  // public OrderService() { }
  // public OrderService(ILogger logger) { }

  // Best practice: exactly one public constructor for DI
  public OrderService(ILogger logger) { _logger = logger; }
}
Real-world example Debugging a confusing DI resolution failure traced back to a class accidentally exposing multiple public constructors.

Common follow-ups: Do all third-party DI containers handle multiple-constructor ambiguity the same way as the built-in ASP.NET Core container?

Fundamentals