Learnixo
Back to blog
AI Systemsintermediate

Dependency Inversion Principle — Depend on Abstractions

Apply the Dependency Inversion Principle in C#: high-level modules depending on interfaces, DI container wiring, avoiding the new keyword for dependencies, and the difference between DIP and dependency injection.

Asma Hafeez KhanMay 16, 20264 min read
SOLIDDIPDependency InjectionC#.NETArchitecture
Share:𝕏

What DIP Means

Dependency Inversion Principle:
  A. High-level modules should not depend on low-level modules.
     Both should depend on abstractions.

  B. Abstractions should not depend on details.
     Details (implementations) should depend on abstractions.

"High-level": business logic, use cases, application services
"Low-level":  database access, email sending, file I/O, clock, random

Without DIP: business logic is coupled to the database technology.
  Changing SQL Server → PostgreSQL requires changing the business logic.

With DIP: business logic depends on IRepository (abstraction).
  The implementation depends on EF Core — not the business logic.

DIP Violation

C#
// Violates DIP: PrescriptionService creates and owns its dependencies
public sealed class PrescriptionService
{
    private readonly ApplicationDbContext _db;   // concrete EF Core context
    private readonly SmtpEmailSender _email;     // concrete SMTP implementation
    private readonly UtcClock _clock;            // concrete clock

    public PrescriptionService()
    {
        _db    = new ApplicationDbContext();     // new = hard coupling
        _email = new SmtpEmailSender("smtp.hospital.local");
        _clock = new UtcClock();
    }

    public async Task CreateAsync(CreatePrescriptionCommand cmd, CancellationToken ct)
    {
        // Hard to test: can't replace _db with a fake, can't replace _email
        // Can't change SMTP server without modifying PrescriptionService
    }
}

DIP Applied

C#
// DIP compliant: depends on abstractions (interfaces), not concrete classes
public sealed class PrescriptionService
{
    private readonly IPrescriptionRepository _repo;  // abstraction
    private readonly IEmailNotifier          _email; // abstraction
    private readonly IClock                  _clock; // abstraction

    public PrescriptionService(
        IPrescriptionRepository repo,
        IEmailNotifier          email,
        IClock                  clock)
    {
        _repo  = repo;
        _email = email;
        _clock = clock;
    }

    public async Task CreateAsync(CreatePrescriptionCommand cmd, CancellationToken ct)
    {
        var now = _clock.UtcNow;  // testable — no DateTime.UtcNow hard-coding
        var prescription = Prescription.Create(/* ... */);
        await _repo.AddAsync(prescription, ct);
        await _email.SendPrescriptionCreatedAsync(prescription, ct);
    }
}

// Interfaces live at the application layer or domain layer
public interface IPrescriptionRepository { Task AddAsync(Prescription p, CancellationToken ct); }
public interface IEmailNotifier          { Task SendPrescriptionCreatedAsync(Prescription p, CancellationToken ct); }
public interface IClock                  { DateTime UtcNow { get; } }

// Implementations live in infrastructure — they depend on abstractions too
public sealed class EfCorePrescriptionRepository : IPrescriptionRepository { /* uses DbContext */ }
public sealed class SmtpEmailNotifier            : IEmailNotifier          { /* uses SMTP */ }
public sealed class SystemClock                  : IClock                  { public DateTime UtcNow => DateTime.UtcNow; }

DI Container Wiring

C#
// Program.cs: wire abstractions to implementations
builder.Services.AddScoped<IPrescriptionRepository, EfCorePrescriptionRepository>();
builder.Services.AddScoped<IEmailNotifier, SmtpEmailNotifier>();
builder.Services.AddSingleton<IClock, SystemClock>();

// PrescriptionService receives its dependencies via constructor injection
builder.Services.AddScoped<PrescriptionService>();

// In tests: swap implementations without changing PrescriptionService
services.AddScoped<IPrescriptionRepository, FakePrescriptionRepository>();
services.AddScoped<IEmailNotifier, FakeEmailNotifier>();
services.AddSingleton<IClock, FakeClock>();

DIP vs Dependency Injection

Dependency Injection (DI):
  A technique for passing dependencies to a class from outside.
  Constructor injection is the most common form.
  DI can exist without interfaces.

Dependency Inversion Principle (DIP):
  A design principle — depend on abstractions, not concrete types.
  DIP requires interfaces (or abstract classes).

The confusion:
  DI without DIP: inject concrete SqlRepository — still hard-coded to SQL Server
  DIP without DI:  depend on IRepository, but new it up inside the class — not injected

DIP + DI together:
  Inject interfaces via DI → highest flexibility for testing and swapping implementations

Abstracting External Time and Randomness

C#
// IClock: abstract time for testability
public interface IClock { DateTime UtcNow { get; } }

public sealed class SystemClock : IClock
{
    public DateTime UtcNow => DateTime.UtcNow;
}

public sealed class FakeClock : IClock
{
    public DateTime UtcNow { get; set; } = DateTime.UtcNow;
    public void Advance(TimeSpan duration) => UtcNow = UtcNow.Add(duration);
}

// Test: prescriptions expire after 30 days
[Fact]
public async Task Prescription_ExpiresAfter30Days()
{
    var clock = new FakeClock();
    var sut   = new PrescriptionExpiryChecker(clock);

    var prescription = CreateActivePrescription(issuedAt: clock.UtcNow);

    clock.Advance(TimeSpan.FromDays(31));
    var result = sut.IsExpired(prescription);

    result.Should().BeTrue();
}

// Without IClock: DateTime.UtcNow is in every method — cannot control time in tests

Production issue I've seen: A pharmacy service had DateTime.UtcNow called directly in the prescription expiry checker. A test was written that created a prescription "today" and checked if it was expired "today". The test passed in development, but failed in CI on the last day of the month (because the system clock ticked to midnight during the test run). Injecting IClock and controlling time in tests eliminated all date-related flakiness.


Key Takeaway

DIP means your business logic depends on interfaces, not concrete classes. Infrastructure (database, email, clock) provides implementations — high-level modules don't know which. Use DI to inject interfaces. Abstract the clock (IClock), file system (IFileStorage), and randomness (IRandomProvider) — these are the hidden dependencies that make code non-deterministic and hard to test. DIP + DI = code that's testable, swappable, and resilient to technology changes.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.