Why Your .NET Dependency Injection is a Dumpster Fire
Dependency Injection (DI) is one of those things that sounds great in theory, but in practice many devs take it too far, abstracting everything into oblivion until their code is an unreadable mess of interfaces, factories, and misplaced optimism.

What We’ll Cover
✅ What DI actually solves (and why it exists in the first place)
✅ Common DI mistakes that make your life miserable
✅ How to structure DI properly in a maintainable way
✅ DI frameworks and how to know if you actually need one
✅ The right way to fix a broken DI setup
By the end, you’ll be able to confidently wield DI like a pro—without turning your code into a flaming wreck.
1. What Did We Do Before Dependency Injection?
To understand why DI exists, let’s take a trip back to the dark ages before it.
Before DI, dependencies were created inside the class that needed them. A service might look like this:
public class OrderService
{
private readonly EmailService _emailService = new EmailService();
private readonly PaymentProcessor _paymentProcessor = new PaymentProcessor();
public void ProcessOrder(Order order)
{
_paymentProcessor.Charge(order);
_emailService.SendConfirmation(order.CustomerEmail);
}
}
Why is this a problem?
🚨 Tight Coupling – OrderService
is hardwired to EmailService
and PaymentProcessor
. Changing dependencies means modifying this class.
🚨 Hard to Test – No easy way to mock EmailService
for unit tests.
🚨 Poor Maintainability – Any change to EmailService
could force changes in every class that uses it.
Enter Dependency Injection
Instead of creating dependencies inside a class, DI injects them from the outside:
public class OrderService
{
private readonly IEmailService _emailService;
private readonly IPaymentProcessor _paymentProcessor;
public OrderService(IEmailService emailService, IPaymentProcessor paymentProcessor)
{
_emailService = emailService;
_paymentProcessor = paymentProcessor;
}
public void ProcessOrder(Order order)
{
_paymentProcessor.Charge(order);
_emailService.SendConfirmation(order.CustomerEmail);
}
}
Why is this better?
✅ Decouples dependencies – Swap out implementations easily.
✅ Improves testability – Inject mock dependencies in unit tests.
✅ Increases flexibility and maintainability – Components are independent.
Great! But DI isn’t foolproof. Let’s talk about the ways you’re probably doing it wrong.
2. The “Service Locator” Anti-Pattern: Global State in Disguise
Some devs don’t fully understand DI, so they fall back on a Service Locator, which is just global state with extra steps.
What is the Service Locator?
Instead of injecting dependencies properly, devs use IServiceProvider
to fetch services inside methods:
public class OrderService
{
private readonly IServiceProvider _serviceProvider;
public OrderService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void ProcessOrder(Order order)
{
var emailService = _serviceProvider.GetService<IEmailService>();
var paymentProcessor = _serviceProvider.GetService<IPaymentProcessor>();
paymentProcessor.Charge(order);
emailService.SendConfirmation(order.CustomerEmail);
}
}
Why is this bad?
🚨 Hidden Dependencies – No way to tell what OrderService
actually depends on.
🚨 Breaks Testability – You have to mock the entire IServiceProvider
for testing.
🚨 Encourages Lazy, Sloppy Code – Instead of designing clear, explicit dependencies, devs just grab whatever they need at runtime.
💀 If you’re using IServiceProvider.GetService<T>()
inside methods instead of injecting dependencies upfront, you’ve reinvented static variables—except now they’re harder to debug. Good job.
✅ Fix it: Inject dependencies through the constructor instead of grabbing them at runtime.
3. Over-Injection: When Every Class Has 20 Dependencies
At some point, a dev learns that DI is good, so they inject everything into their constructors.
public class OrderService
{
public OrderService(
IUserRepository userRepository,
IProductRepository productRepository,
IEmailService emailService,
ILoggingService loggingService,
IPaymentProcessor paymentProcessor,
ICacheManager cacheManager,
ISecurityContext securityContext,
IAuditTrail auditTrail,
IFeatureToggleService featureToggleService,
IShippingCalculator shippingCalculator)
{
}
}
What even is this? A service or an IKEA warehouse?
How to Fix It
🔹 Break large services into smaller, focused services
Before: One monolithic class.
public class OrderService { /* 20 dependencies */ }
After: Smaller, single-responsibility classes.
public class OrderProcessor { /* Handles payment and shipping */ }
public class OrderNotifier { /* Handles customer notifications */ }
🔹 Use a Facade Class
Instead of injecting a ton of services, inject a wrapper that groups related dependencies:
public class OrderService
{
private readonly IOrderFacade _orderFacade;
public OrderService(IOrderFacade orderFacade)
{
_orderFacade = orderFacade;
}
}
🔹 Use Lazy Injection for Rarely Used Dependencies
public class OrderService
{
private readonly Lazy<IExpensiveService> _expensiveService;
public OrderService(Lazy<IExpensiveService> expensiveService)
{
_expensiveService = expensiveService;
}
public void DoSomething()
{
_expensiveService.Value.ExpensiveMethod();
}
}
🔹 Use Composition Instead of DI
public class OrderService
{
public void ProcessOrder(Order order, ILogger logger)
{
logger.Log("Processing order");
}
}
This way, ILogger
is only passed when needed—reducing unnecessary dependencies.
4. Understanding DI Lifetime Scopes
Many bugs come from using the wrong lifetime scope. Here’s a quick breakdown:
Scope | Description | Best For | Issues If Used Incorrectly |
---|---|---|---|
Transient | New instance every time | Stateless, lightweight services | Performance overhead |
Scoped | One instance per request | Web apps (per-request services) | Injecting into a singleton causes state bugs |
Singleton | One instance for app lifetime | Caches, shared services | Risk of memory leaks, unintended shared state |
🚨 Rule of thumb: If you're unsure, reread this table. Just because it works doesn’t mean it’s the right choice.
5. When to Use a DI Framework
🚀 Use .NET’s built-in DI unless…
✅ You need property injection
✅ You need child containers (e.g., per-request scope in a worker service)
✅ You need dynamic service registration
Other Frameworks
- Autofac – Most popular alternative, flexible, property injection, better lifetime management
- SimpleInjector – Lightweight, high-performance. Great for performance-critical apps.
- Castle Windsor, Unity, StructureMap, Ninject – Older, less common today
Fixing Your DI Mess
So now you have this information, you finally understand what dependency injection is, why you should use it, and most importantly, how to use it correctly, you open up your code, but… Oh no… this code is terrible. Service locator, tightly coupled classes, massive constructors, and you’re feeling deflated. Well, worry not, there is hope. For only $49.99 per month… Just joking. You can actually start fixing up your code and using DI correctly bit by bit.
✅ Start by refactoring giant constructors (Lazy loading, refactoring into smaller services, composition, facade)
✅ Kill the Service Locator pattern (no explanation needed, just kill it)
✅ Use DI strategically, not everywhere (go back to the top of this post and remember what problem it solves and why we use it. Let that inform you where you need it)
Now go forth and make your DI not a dumpster fire. 🔥