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.

Why Your .NET Dependency Injection is a Dumpster Fire
Dependencies Successfully Injected... Wait, what?

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 CouplingOrderService 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:

ScopeDescriptionBest ForIssues If Used Incorrectly
TransientNew instance every timeStateless, lightweight servicesPerformance overhead
ScopedOne instance per requestWeb apps (per-request services)Injecting into a singleton causes state bugs
SingletonOne instance for app lifetimeCaches, shared servicesRisk 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. 🔥