
C# Dependency Patterns: The Good, The Bad & The Ugly
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
- Prefer constructor injection by default
- Use ambient context for testable globals — sparingly
- Use static only for pure utility logic
- Treat the service locator as technical debt
- Use property/method injection only for framework or strategy use cases
Leave a Reply
Your e-mail address will not be published. Required fields are marked *