TL;DR

  • The Dependency Inversion Principle (DIP) means depend on abstractions, not concrete classes.
  • Use ASP.NET Core DI to inject interfaces and swap implementations without changing code.
  • DIP makes your code flexible, testable, and easy to maintain.
  • Only abstract behavior when you need flexibility or multiple implementations.

Ever hardcoded a service like new EmailService() in your controller and later needed to swap it for something else? Maybe you needed to send SMS instead of emails, or switch to a different email provider. That’s where the Dependency Inversion Principle saves you from painful refactoring.

Before: Hardcoded Dependency

Here’s what tightly-coupled code looks like:

[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> CreateUser(CreateUserRequest request)
    {
        var user = new User { Email = request.Email, Name = request.Name };
        
        // Hardcoded dependency - this is the problem
        var emailService = new EmailService();
        await emailService.SendWelcomeEmailAsync(user.Email, user.Name);
        
        return Ok();
    }
}

public class EmailService
{
    public async Task SendWelcomeEmailAsync(string email, string name)
    {
        // Send email logic
        Console.WriteLine($"Sending welcome email to {email}");
    }
}

This works, but your controller is stuck with email forever. Want to switch to SMS? You’re rewriting controller code.

After: Abstracting with Interfaces

Here’s the same logic using the Dependency Inversion Principle:

public interface INotificationSender
{
    Task SendWelcomeMessageAsync(string recipient, string name);
}

public class EmailService : INotificationSender
{
    public async Task SendWelcomeMessageAsync(string recipient, string name)
    {
        // Email logic
        Console.WriteLine($"Sending welcome email to {recipient}");
    }
}

[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    private readonly INotificationSender _notificationSender;

    public UserController(INotificationSender notificationSender)
    {
        _notificationSender = notificationSender; // Injected, not hardcoded
    }

    [HttpPost]
    public async Task<IActionResult> CreateUser(CreateUserRequest request)
    {
        var user = new User { Email = request.Email, Name = request.Name };
        
        // Controller doesn't care HOW notifications are sent
        await _notificationSender.SendWelcomeMessageAsync(user.Email, user.Name);
        
        return Ok();
    }
}

Register it in your DI container:

// In Program.cs
builder.Services.AddScoped<INotificationSender, EmailService>();

Why This Matters

Now your controller depends on the behavior (INotificationSender) instead of a specific implementation (EmailService). This flips the dependency, instead of depending on concrete classes, you depend on abstractions.

Want to switch to SMS? Just create a new implementation:

public class SmsService : INotificationSender
{
    public async Task SendWelcomeMessageAsync(string recipient, string name)
    {
        // SMS logic
        Console.WriteLine($"Sending welcome SMS to {recipient}");
    }
}

And update your registration:

builder.Services.AddScoped<INotificationSender, SmsService>();

Your controller code stays exactly the same. Zero changes needed.

The Real Win

You can even switch implementations based on configuration:

if (builder.Configuration.GetValue<bool>("UseSms"))
    builder.Services.AddScoped<INotificationSender, SmsService>();
else
    builder.Services.AddScoped<INotificationSender, EmailService>();

DIP isn’t about creating interfaces everywhere, it’s about depending on what something does, not how it does it. When you need flexibility in how your app behaves, abstract the behavior and let DI handle the rest.

Your code becomes easier to test, swap, and maintain. That’s the power of depending on abstractions instead of concrete implementations.

Related Posts