Dependency Inversion Principle in C#: Flipping Dependencies for Cleaner Architecture

In most apps, business logic ends up relying directly on infrastructure—file systems, APIs, databases. That’s fine… until it isn’t. Ever tried to unit test a service that talks straight to SQL Server? Or change a logger and had to rewrite half your app? That’s where the Dependency Inversion Principle (DIP) comes in—the "D" in SOLID. High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions. Let’s break this down without overcomplicating it. What It Means At its core, Dependency Inversion is about decoupling. Instead of this: Business Logic → Logger We flip it: Business Logic → ILogger ← Logger Now your logic doesn’t care how something is logged—it just knows it can call ILogger.Log(). The actual implementation? Plug it in later. The Wrong Way: Tight Coupling Let’s say you have this: public class InvoiceService { private readonly FileLogger _logger = new FileLogger(); public void Process(Invoice invoice) { _logger.Log("Processing invoice..."); // logic here } } This works, but now: You can’t reuse InvoiceService with a different logger. It’s harder to test. It violates DIP—InvoiceService depends directly on the concrete FileLogger. ✅ The Better Way: Invert the Dependency Let’s extract an interface: public interface ILogger { void Log(string message); } Now have FileLogger implement it: public class FileLogger : ILogger { public void Log(string message) { Console.WriteLine($"[FILE] {message}"); } } Then inject the abstraction: public class InvoiceService { private readonly ILogger _logger; public InvoiceService(ILogger logger) { _logger = logger; } public void Process(Invoice invoice) { _logger.Log("Processing invoice..."); } } You’ve now inverted the dependency. InvoiceService depends only on what it needs—a logging contract, not a concrete file-based logger. Now It’s Testable You can easily mock or fake the logger in a unit test: public class FakeLogger : ILogger { public void Log(string message) { } } var service = new InvoiceService(new FakeLogger()); service.Process(testInvoice); DIP makes testing cleaner and faster—no hacks, no tightly coupled dependencies. Real-World Uses Swap databases without rewriting services Switch between email/SMS/Slack notifications without touching business logic Plug in different payment gateways behind the same interface DIP isn’t about patterns for the sake of patterns—it’s about protecting your core logic from infrastructure churn.

Mar 26, 2025 - 13:51
 0
Dependency Inversion Principle in C#: Flipping Dependencies for Cleaner Architecture

In most apps, business logic ends up relying directly on infrastructure—file systems, APIs, databases. That’s fine… until it isn’t.

Ever tried to unit test a service that talks straight to SQL Server? Or change a logger and had to rewrite half your app?

That’s where the Dependency Inversion Principle (DIP) comes in—the "D" in SOLID.

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Abstractions should not depend on details. Details should depend on abstractions.

Let’s break this down without overcomplicating it.

What It Means

At its core, Dependency Inversion is about decoupling.

Instead of this:

Business Logic → Logger

We flip it:

Business Logic → ILogger ← Logger

Now your logic doesn’t care how something is logged—it just knows it can call ILogger.Log(). The actual implementation? Plug it in later.

The Wrong Way: Tight Coupling

Let’s say you have this:

public class InvoiceService
{
    private readonly FileLogger _logger = new FileLogger();

    public void Process(Invoice invoice)
    {
        _logger.Log("Processing invoice...");
        // logic here
    }
}

This works, but now:

  • You can’t reuse InvoiceService with a different logger.
  • It’s harder to test.
  • It violates DIP—InvoiceService depends directly on the concrete FileLogger.

✅ The Better Way: Invert the Dependency

Let’s extract an interface:

public interface ILogger
{
    void Log(string message);
}

Now have FileLogger implement it:

public class FileLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[FILE] {message}");
    }
}

Then inject the abstraction:

public class InvoiceService
{
    private readonly ILogger _logger;

    public InvoiceService(ILogger logger)
    {
        _logger = logger;
    }

    public void Process(Invoice invoice)
    {
        _logger.Log("Processing invoice...");
    }
}

You’ve now inverted the dependency. InvoiceService depends only on what it needs—a logging contract, not a concrete file-based logger.

Now It’s Testable

You can easily mock or fake the logger in a unit test:

public class FakeLogger : ILogger
{
    public void Log(string message) {  }
}
var service = new InvoiceService(new FakeLogger());
service.Process(testInvoice);

DIP makes testing cleaner and faster—no hacks, no tightly coupled dependencies.

Real-World Uses

  • Swap databases without rewriting services
  • Switch between email/SMS/Slack notifications without touching business logic
  • Plug in different payment gateways behind the same interface

DIP isn’t about patterns for the sake of patterns—it’s about protecting your core logic from infrastructure churn.