Dependency Injection & IoC Principles

10 questions found

What is dependency injection, and what problem does it solve compared to a class creating its own dependencies directly?

Beginner
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

How do you register and resolve a service using ASP.NET Core's built-in dependency injection container?

Beginner
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

What is the difference between Singleton, Scoped, and Transient service lifetimes in ASP.NET Core's DI container?

Intermediate
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

What is the Inversion of Control (IoC) principle, and how does dependency injection relate to it as one specific technique?

Intermediate
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#

How does constructor injection differ from property (setter) injection, and why is constructor injection generally preferred?

Intermediate
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

What causes a 'captive dependency' bug, and how does ASP.NET Core's DI container detect and prevent it?

Advanced
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

How would you implement the Factory pattern using dependency injection to resolve a specific implementation at runtime based on a parameter, when the standard container can't decide statically?

Advanced
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#

How do you correctly and safely resolve a Scoped service from within a Singleton service using IServiceScopeFactory, avoiding the captive dependency problem?

Advanced
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

How would you write a custom IServiceProviderFactory or use a third-party container (like Autofac) to enable advanced DI features not supported by the built-in ASP.NET Core container, like property injection or convention-based registration?

Advanced
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#

How does the DI container resolve a class with MULTIPLE constructors, and how do you influence which one gets chosen?

Advanced
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