SOLID Principles in C#: The Complete Guide
Software that starts clean rarely stays that way on its own. Requirements pile up. Features stack. Deadlines push you toward shortcuts. One class does six things. Tests become impossible to write. Every bug fix introduces two new bugs nobody expected. SOLID principles C# (the solid principles c# that drive clean design) are the five design guidelines that prevent this decay -- when applied consistently.
Robert C. Martin (Uncle Bob) formalized the SOLID acronym in the early 2000s. The underlying ideas stretch back further. The principles aren't C#-specific -- they apply to any object-oriented language. But C# and .NET give you excellent tools for applying them, and the modern .NET 10 ecosystem makes solid principles c# feel natural rather than forced.
This guide covers all five solid principles c# in depth. For each principle, you'll see a concrete code violation, understand the damage it causes, and understand what better looks like. You'll also see how the principles interact with each other and how modern C# features support solid design.
Why SOLID Principles C# Matter to Every .NET Developer
The acronym breaks down like this:
- S -- Single Responsibility Principle (SRP): A class should have one reason to change.
- O -- Open/Closed Principle (OCP): Open for extension, closed for modification.
- L -- Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types.
- I -- Interface Segregation Principle (ISP): Clients should not depend on interfaces they don't use.
- D -- Dependency Inversion Principle (DIP): Depend on abstractions, not concretions.
These are the solid principles c# that define modern .NET architecture:
These five solid principles c# are a system, not a checklist. Each one addresses a different failure mode. Violating one creates pressure on the others. Applying all five consistently compounds the benefit: you get code that is genuinely easy to change -- because you can change one thing at a time, in one place, with confidence.
Single Responsibility Principle in C#
Every class should have one reason to change -- this is one of the five solid principles c# that shapes clean class design. That's the entire definition. But "reason" is the tricky word. It doesn't mean one method or one line of code. It means one actor -- one stakeholder, one business domain, one concern -- whose requirements might drive changes to that class.
Here's a classic violation. One class handles authentication, email sending, and database writes -- three completely separate concerns:
// VIOLATION: Three separate concerns packed into one class
public class UserManager
{
private readonly string _connectionString;
public UserManager(string connectionString)
{
_connectionString = connectionString;
}
// Concern 1: Authentication
public bool Authenticate(string username, string password)
{
using var conn = new SqlConnection(_connectionString);
// Validate credentials against the database
return true;
}
// Concern 2: Email notification
public void SendWelcomeEmail(string email, string username)
{
using var client = new SmtpClient("smtp.example.com");
var message = new MailMessage(
"[email protected]", email,
"Welcome!", $"Hello {username}, welcome aboard.");
client.Send(message);
}
// Concern 3: Persistence
public void SaveUser(User user)
{
using var conn = new SqlConnection(_connectionString);
// Execute INSERT into the users table
}
}
This class changes when authentication logic changes. It changes when email templates change. It changes when the database schema changes. Three different stakeholders can independently demand changes to this one class. That's three reasons to change -- a direct SRP violation.
A common fix is decomposition into focused, single-concern classes: AuthenticationService, EmailService, and UserRepository. Each becomes independently testable, independently deployable, and independently maintainable. We cover this in depth -- including real-world heuristics for spotting violations -- in our complete guide on the Single Responsibility Principle.
Open/Closed Principle in C# -- Why It Matters
Once a class is written and tested, you should be able to add new behavior without modifying it. The solid principles c# OCP rule means: open for extension through new implementations, closed for modification because existing tested code stays untouched.
The most common OCP violation is a switch or if-else chain that must be opened every time a new requirement arrives:
// VIOLATION: Every new report format forces a change to this method
public class ReportExporter
{
public byte[] Export(Report report, string format)
{
if (format == "PDF")
return GeneratePdf(report);
else if (format == "CSV")
return GenerateCsv(report);
else if (format == "Excel")
return GenerateExcel(report);
// Adding XML export? Open this file. Adding JSON? Open it again.
throw new NotSupportedException($"Format '{format}' is not supported.");
}
private byte[] GeneratePdf(Report report) => Array.Empty<byte>();
private byte[] GenerateCsv(Report report) => Array.Empty<byte>();
private byte[] GenerateExcel(Report report) => Array.Empty<byte>();
}
Adding XML export means editing ReportExporter. Adding JSON means editing it again. Tests for PDF generation now sit in the same class as XML logic. Changes compound risk.
The OCP-compliant version introduces an IReportExporter interface. Each format becomes its own class. The consumer depends on the abstraction and a registry -- no modification needed when new formats arrive, only new classes.
The Template Method Design Pattern and the Visitor Design Pattern in C# are both classic OCP enablers -- they let you add new operations or behaviors without touching the core algorithm. We explore OCP further in our dedicated guide on the Open/Closed Principle.
Liskov Substitution Principle in C#
If S is a subtype of T, you should be able to use S anywhere T is expected without breaking the program. No surprises. No silently wrong results. No unexpected exceptions. The subclass must honor the behavioral contract of the parent -- not just the method signatures.
Here's the canonical rectangle/square example:
// VIOLATION: Square silently breaks the behavioral contract of Rectangle
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area() => Width * Height;
}
public class Square : Rectangle
{
// A square must keep width == height, so both setters sync each other
public override int Width
{
set { base.Width = value; base.Height = value; }
}
public override int Height
{
set { base.Width = value; base.Height = value; }
}
}
// This consumer breaks when you pass a Square -- it does nothing wrong itself
public static void DoubleWidth(Rectangle r)
{
int originalArea = r.Area();
r.Width *= 2;
// Expected: area doubled. Actual for Square: area quadrupled
Console.WriteLine($"Expected {originalArea * 2}, got {r.Area()}");
}
The consumer sets width and height independently, which is a completely reasonable thing to do with a Rectangle. Square silently violates that expectation. Substituting a Square for a Rectangle produces wrong results -- a textbook LSP violation that's impossible to detect at compile time.
A common fix is to stop using inheritance when you're modeling different behaviors. Square and Rectangle are mathematically distinct. They should implement a common IShape interface rather than one extending the other. Favor composition and shared interfaces over inheritance when behaviors diverge. We cover practical LSP guidance -- including substitution contracts and precondition/postcondition rules -- in our dedicated guide on the Liskov Substitution Principle.
Interface Segregation Principle in C#
Don't force clients to depend on methods they don't use. Fat interfaces are the ISP violation in its most common form -- one interface trying to cover every role and scenario, forcing implementors to stub out behavior that doesn't apply to them.
// VIOLATION: One interface trying to represent every kind of employee
public interface IWorker
{
void Work();
void TakeBreak();
void SubmitTimesheet();
void ManageTeam(); // Only managers do this
void WriteCode(); // Only developers do this
void HandleSupport(); // Only support staff do this
}
// A contractor is forced to stub out two methods that don't apply
public class Contractor : IWorker
{
public void Work() { /* real implementation */ }
public void TakeBreak() { /* real implementation */ }
public void SubmitTimesheet() { /* real implementation */ }
// Forced to exist -- no valid implementation for a contractor
public void ManageTeam() => throw new NotSupportedException("Contractors don't manage teams.");
public void WriteCode() { /* real implementation */ }
public void HandleSupport() => throw new NotSupportedException();
}
ManageTeam and HandleSupport have no business on a Contractor. The interface is leaking concerns from multiple roles into one bloated definition. Every time the IWorker interface grows, every implementor must be updated.
The ISP-compliant version splits the interface into role-specific contracts: IDeveloper, IManager, ISupportStaff, and a shared IEmployee for truly universal behavior. Each client depends only on what it actually uses. The Mediator Design Pattern pairs naturally here -- when components need to interact without being coupled to each other's full interface, the mediator provides a narrow communication channel. We go deeper on ISP in our dedicated guide on the Interface Segregation Principle.
Dependency Inversion Principle in C#
High-level modules -- business logic, orchestration code -- should not depend on low-level modules like database drivers, HTTP clients, or file I/O. Both should depend on abstractions. This is the principle that makes dependency injection feel like the natural default in modern .NET.
// VIOLATION: OrderService creates its own low-level dependencies
public class OrderService
{
// Hard dependency on a concrete class -- impossible to unit test without a real database
private readonly SqlOrderRepository _repository = new SqlOrderRepository("Server=.;...");
private readonly SmtpEmailSender _emailSender = new SmtpEmailSender("smtp.example.com");
public async Task PlaceOrderAsync(Order order)
{
await _repository.SaveAsync(order);
await _emailSender.SendConfirmationAsync(order.CustomerEmail, order);
}
}
// CORRECT: OrderService depends on abstractions injected at construction time
public interface IOrderRepository { Task SaveAsync(Order order); }
public interface IEmailSender { Task SendConfirmationAsync(string email, Order order); }
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IEmailSender _emailSender;
// Dependencies injected -- testable, swappable, honest about what they need
public OrderService(IOrderRepository repository, IEmailSender emailSender)
{
_repository = repository;
_emailSender = emailSender;
}
public async Task PlaceOrderAsync(Order order)
{
await _repository.SaveAsync(order);
await _emailSender.SendConfirmationAsync(order.CustomerEmail, order);
}
}
The second version of OrderService depends on IOrderRepository, not SqlOrderRepository. Tests inject mocks. A future migration to a different database or email provider touches only the concrete implementation, never the business logic.
The Proxy Design Pattern and the Facade Design Pattern both rely on DIP to function -- they sit behind interfaces that high-level modules consume, hiding low-level complexity behind a clean abstraction boundary. We cover DIP patterns extensively in our guide on the Dependency Inversion Principle.
Real-world violations are often subtler than these canonical examples.
How SOLID Principles Work Together
The five solid principles c# are a system, not a cafeteria menu. Apply one in isolation and you get partial improvement. Apply all five and the benefits compound.
Start with SRP. Small, focused classes are natural candidates for narrow interfaces -- which directly serves ISP. Narrow interfaces are easy to depend on abstractly -- which is DIP. When each class does one thing, extending the system means adding new classes rather than modifying existing ones -- which is OCP. Class hierarchies that represent genuine relationships don't trip over behavioral contracts -- which keeps LSP clean.
The failure cascade runs the same way in reverse. A god class (SRP violation) is impossible to mock in tests (DIP violation), forces every client to import a bloated interface (ISP violation), and demands constant modification when new requirements arrive (OCP violation). The principles often reinforce each other. Fixing SRP often resolves violations of the others automatically.
This scaling applies beyond individual classes. When you're building a modular monolith in C#, SOLID applied at the module level is what keeps the architecture clean. Each module has a single responsibility, depends on abstractions at its boundaries, and exposes a narrow interface to the outside world.
Even in a traditional monolith architecture, solid principles c# prevent the specific failure mode that gives monoliths a bad reputation: tightly coupled layers, untestable business logic, and a codebase where nobody feels confident making changes.
SOLID Principles in .NET 10
The solid principles c# haven't changed. But modern .NET -- including .NET 10 -- gives you excellent tools to apply them, and the framework includes many SOLID-friendly abstractions.
Records enforce SRP on data containers. A positional record like record UserProfile(string Name, string Email) is unmistakably a data carrier. No business logic belongs there. This boundary between data and behavior is enforced by the type system, not just convention.
Built-in DI makes DIP the default mode of operation. The Microsoft.Extensions.DependencyInjection container is designed around interface registration. This is DIP by convention:
var builder = WebApplication.CreateBuilder(args);
// Register abstractions, not concretions -- DIP as the default pattern
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddLogging(); // ILogger<T> is the framework's own DIP example
var app = builder.Build();
ILogger<T> is the canonical DIP example in the framework. You inject ILogger<T> -- not a concrete logger. Your class doesn't care whether logs go to the console, Application Insights, or a custom sink. The logging in .NET complete guide shows how this plays out in production scenarios where swappability and testability both matter.
ASP.NET Core is OCP at the framework level. You implement IMiddleware, IActionFilter, or IHostedService to extend the framework without modifying any framework code. New behavior arrives as new classes. Existing pipeline code stays untouched.
C# interface features can support SOLID-oriented design. Default interface methods (available since C# 8, continuing in .NET 10) let you add behavior to interfaces without breaking existing implementors. Static abstract members (available since C# 11 / .NET 7, continuing in .NET 10) can support certain patterns in generic algorithms. These features can reduce the friction of applying solid principles c# -- not by changing the principles themselves, but by making certain patterns easier to express.
FAQ
What are the SOLID principles in C#?
SOLID is an acronym for five object-oriented design principles: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. Each principle targets a specific failure mode in object-oriented design. Together they guide C# code toward classes that are easy to maintain, extend, test, and reason about.
Which SOLID principle is most important?
SRP is a productive place to start. When classes do one thing, the other four principles become significantly easier to apply. A codebase full of god classes makes OCP, ISP, and DIP significantly harder to maintain consistently. If you can only focus on one principle at a time, starting with SRP is often a natural entry point -- the rest tends to follow.
Are SOLID principles only for large codebases?
No. SRP and DIP are valuable in small projects because they make unit testing possible from day one. OCP and ISP matter more as the system grows. ISP violations are common even in small codebases when developers reflexively create one large interface rather than composing several focused ones. The scale at which each principle pays off varies; none of them are irrelevant.
How do SOLID principles relate to design patterns?
Design patterns are implementation strategies -- and many exist specifically to enable SOLID principles. The Strategy pattern enables OCP. The Repository pattern enables DIP. The Facade pattern enables both ISP and DIP. Understanding solid principles c# first makes it much easier to understand why patterns exist and when to apply them, rather than memorizing them as solutions in search of problems.
Do SOLID principles conflict with performance optimization?
Occasionally, in specific hot paths. More abstraction layers mean more indirection, and virtual dispatch has measurable overhead in tight loops. But most performance problems are not in the architectural layer -- they're in algorithms, I/O patterns, and allocations. Start with SOLID design. Profile with benchmarks. Only break a principle when measurements prove it's necessary, not as a preemptive optimization.
Can I violate SOLID principles intentionally?
Yes. Pragmatism matters. A small utility class that formats a timestamp doesn't need to be decomposed into four focused collaborators. The principles are guidelines, not laws. The goal is maintainable, testable code. Apply solid principles c# where they prevent real pain -- in complex, frequently changing business logic. Don't apply them where they'd create unnecessary ceremony.
How do I enforce SOLID principles in a team?
Code review is the primary mechanism. When you see a god class or a constructor instantiating its own dependencies, point it out, explain the principle it violates, and show the refactored version. Automated tools like NDepend can flag dependency cycles and coupling metrics. Writing unit tests enforces DIP naturally -- if you can't inject a mock, the class almost certainly violates DIP. The discipline sticks when developers connect the principle to a real bug they experienced, not an abstract rule they memorized.
Conclusion
The solid principles c# aren't complicated individually. The discipline is applying them consistently -- under deadline pressure, against an existing codebase, when the "quick fix" is always visible and the SOLID fix requires a bit more thought.
SRP is often a productive place to start. Once your classes are focused, the other four principles tend to follow more naturally. Each principle has a dedicated guide that goes deep on real violations, refactoring strategies, and .NET 10 patterns.
If you're picking a starting point, the Single Responsibility Principle is worth considering first.

