Constructor Injection (The Gold Standard)

public OrderService(ILogger logger) { ... }

Why this should cover 90% of your dependency needs:

  • Compile-time safety — the compiler enforces what the class requires
  • Explicit dependencies — easy to read and maintain
  • Test-friendly — mocking frameworks can inject fakes effortlessly
  • Works seamlessly with DI containers like ASP.NET Core's built-in one

Constructor injection promotes immutability and makes classes easier to reason about. If something is required for your class to function, make it a constructor dependency. Keep it simple and clear.

Ambient Context (Smart Static)

AppTime.Now = () => testDate; // Test override

Use for:

  • Time providers (Clock.Now)
  • Request-specific context (CurrentUser.Id)
  • Tenant ID in multi-tenant apps (TenantContext.CurrentTenantId)

This pattern creates globally accessible values that can be overridden in test environments. It offers the convenience of static access without sacrificing testability — often implemented with AsyncLocal<T> to preserve context across async calls.

Watch out:

  • Hidden dependencies hurt clarity
  • Easy to misuse as a global state
  • Needs careful thread-safety handling

Used well, ambient context enables testable infrastructure. Used poorly, it becomes global spaghetti.

Static Utilities

public static double CalculateTax(double amount) { ... }

Use only when:

  • Pure function — no side effects
  • Stateless — no internal state
  • No dependencies — no I/O, config, or services
  • Thread-safe

Static methods are fast and simple — perfect for math helpers, string formatters, or extension methods. But avoid putting business logic or infrastructure code in static classes. You lose flexibility, testability, and DI support.

Service Locator

var logger = ServiceLocator.Get<ILogger>();

Why it's dangerous:

  • Obscures what a class really needs
  • Forces tests to work around the global state
  • Fails at runtime instead of compile time

Acceptable only in:

  • Legacy migration phases
  • Framework internals where DI isn't available

It’s the “go-to” of dependency injection — tempting, but toxic. Avoid unless you're refactoring legacy code and need a temporary bridge.

Property/Method Injection

[Inject] public ILogger Logger { get; set; }
// or
public void Process(IPaymentStrategy strategy) { ... }

Use when:

  • Required by UI frameworks (Blazor, WPF, etc.)
  • Injecting optional or interchangeable strategies
  • Late-bound behavior or plugins

Useful for specific patterns, but harder to validate and mock. Prefer constructor injection unless the framework or pattern demands otherwise.

The Hard Truth

  1. Prefer constructor injection by default
  2. Use ambient context for testable globals — sparingly
  3. Use static only for pure utility logic
  4. Treat the service locator as technical debt
  5. Use property/method injection only for framework or strategy use cases