SOLID in Real .NET Projects — Violations and Fixes
Apply all five SOLID principles together in a real .NET project: recognizing violations in existing code, refactoring to SOLID step by step, the cost-benefit analysis of SOLID, and when NOT to apply a principle.
Recognizing All Five Violations at Once
// This single class violates all five SOLID principles
public class PatientManager
{
private ApplicationDbContext _db = new ApplicationDbContext();
// SRP: handles CRUD, reporting, notifications, and scheduling — too many responsibilities
public void SavePatient(Patient p) { _db.Patients.Add(p); _db.SaveChanges(); }
public byte[] GeneratePatientReport(Patient p) // report logic in "manager"?
{
if (reportType == "PDF") return GeneratePdf(p); // OCP: new type = modify this
else if (reportType == "Excel") return GenerateExcel(p);
throw new NotSupportedException();
}
public void Send(string channel, string message) // ISP: single method for all channels
{
if (channel == "SMS") { /* SMS */ }
if (channel == "Fax") { throw new NotImplementedException(); } // LSP violation
}
// DIP: concrete dependencies, not abstractions
}Refactoring Step by Step
Step 1 — Apply SRP: Split the Class
// PatientRepository: only persistence
// PatientReportService: only reporting
// PatientAlertService: only notifications
// PatientScheduler: only scheduling
// Each class has one reason to change:
// PatientRepository: if the database changes
// PatientReportService: if the report format changes
// PatientAlertService: if the notification channel changesStep 2 — Apply OCP: Extension Points for Varying Behavior
// Report formats are extension points — each is a separate class
public interface IReportGenerator { byte[] Generate(Patient patient); }
public sealed class PdfReportGenerator : IReportGenerator { /* ... */ }
public sealed class ExcelReportGenerator : IReportGenerator { /* ... */ }
// Adding HTML: new class, no existing code changesStep 3 — Apply LSP: No NotImplementedException
// Fax channel either fully implements IAlertChannel or is removed
// If Fax cannot send async alerts, it should not implement IAlertChannel
// Consider: ISyncAlertChannel separate interface for faxStep 4 — Apply ISP: Split IAlertChannel by Role
public interface IInstantAlert { Task SendAsync(PatientAlert alert, CancellationToken ct); }
public interface IScheduledAlert { Task ScheduleAsync(PatientAlert alert, DateTime sendAt, CancellationToken ct); }
// SMS implements IInstantAlert only — no ScheduleAsync
// Email implements bothStep 5 — Apply DIP: Inject Abstractions
public sealed class PatientAlertService
{
private readonly IEnumerable<IInstantAlert> _channels;
private readonly IClock _clock;
public PatientAlertService(IEnumerable<IInstantAlert> channels, IClock clock)
{
_channels = channels;
_clock = clock;
}
}The SOLID Trade-off Analysis
SOLID benefits:
✓ Code is easier to test (DIP + SRP)
✓ Features are easier to add (OCP)
✓ Interfaces are simpler (ISP)
✓ Substitutions work correctly (LSP)
SOLID costs:
✗ More files, more interfaces, more indirection
✗ Takes time to design correctly upfront
✗ Overkill for simple, short-lived scripts
✗ Can lead to over-engineering if applied blindly
Apply SOLID where:
→ The code is expected to change (new business rules, new channels, new formats)
→ Tests are written and rely on the design
→ Multiple developers work in the same area
Skip SOLID where:
→ Simple CRUD with no expected variation
→ Throwaway scripts and one-off utilities
→ Early prototyping where requirements are unknownRed Flag / Green Answer
Red Flag: "We apply SOLID to everything — every class has an interface, every behavior has a strategy pattern, every rule has a specification. It's clean code."
Over-applying SOLID produces excessive indirection: a
CreatePatientCommandHandlerthat callsIPatientFactory, which callsIPatientMrnValidator, which callsIMrnFormatPolicy. Reading code requires jumping through 6 abstractions to understand "create a patient." Clean code is readable and proportional to the problem. Apply SOLID to the areas where flexibility and testability are genuinely needed, not everywhere.
Green Answer:
We apply SRP when a class has multiple reasons to change. We apply OCP when we identify a recurring extension pattern (new notification channel, new report format). We apply LSP when we catch
NotImplementedExceptionin method stubs. We apply ISP when interfaces grow beyond 5-6 methods. We apply DIP when a class constructs its own dependencies or usesDateTime.UtcNowdirectly. SOLID is a guide, not a mandate.
Practical Checklist
Before writing a class, ask:
□ What is this class responsible for? (SRP)
□ Where will I need to add new behavior later? (OCP)
□ Does every implementation honor the contract? (LSP)
□ Does this interface have unused methods in some implementations? (ISP)
□ Does this class create its own dependencies? (DIP)
Code review signals:
✗ Class has >200 lines: likely SRP violation
✗ if/switch on type or format: likely OCP violation
✗ NotImplementedException in a method: likely LSP + ISP violation
✗ Interface has >8 methods: likely ISP violation
✗ new ConcreteService() inside a class body: likely DIP violationKey Takeaway
SOLID principles work together: SRP limits class size, OCP uses the extension points SRP creates, LSP makes those extension points reliable, ISP keeps them focused, and DIP makes them injectable and testable. Violations cluster: an SRP violation often breaks OCP (one large class does too many things in an if/else chain). Fixing SRP often also fixes OCP. Apply SOLID proportionally — to code that changes frequently, not to everything.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.