SOLID Principles in C# · Lesson 5 of 6
Dependency Inversion Principle — Depend on Abstractions
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
// 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
// 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
// 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 implementationsAbstracting External Time and Randomness
// 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 testsProduction issue I've seen: A pharmacy service had
DateTime.UtcNowcalled 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). InjectingIClockand 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.