SOLID Principles in C#: Beginner-Friendly Guide to Clean Code
Learn SOLID principles in C# with easy-to-understand examples. Master SRP, OCP, LSP, ISP, and DIP to write clean, maintainable, and scalable code.
Developers often face challenges maintaining applications as they grow. Features get added, bugs appear, and code that once worked perfectly becomes hard to manage. The solution lies in good design principles. One of the most effective sets of principles is SOLID.
SOLID helps developers write code that is easier to understand, extend, and maintain, especially in large-scale applications.
Why Applications Fail Without SOLID
Even experienced developers create applications that become fragile over time. Common design flaws include:
- Too many responsibilities in one class – classes that do too much become difficult to change.
- Tightly coupled classes – changes in one class ripple through the system.
- Duplicate code – making maintenance and updates error-prone.
Solutions
To improve software quality, consider:
- Choosing a suitable architecture (MVC, MVVM, Layered, 3-tier, etc.)
- Applying design principles like SOLID
- Using design patterns appropriately
In this post, we’ll focus on SOLID principles first.
Introduction to SOLID
SOLID stands for:
- S: Single Responsibility Principle (SRP)
- O: Open/Closed Principle (OCP)
- L: Liskov Substitution Principle (LSP)
- I: Interface Segregation Principle (ISP)
- D: Dependency Inversion Principle (DIP)
These principles guide you to write flexible, modular, and maintainable code.
S: Single Responsibility Principle (SRP)
Definition: "Every class should have only one reason to change."
Each class should focus on a single purpose. Having multiple responsibilities in a single class makes it fragile and difficult to test.
Example
public class UserService
{
public void Register(string email, string password)
{
if (!ValidateEmail(email))
throw new ValidationException("Invalid email");
var user = new User(email, password);
SendEmail(new MailMessage("mysite@nowhere.com", email) { Subject="Hello!" });
}
public bool ValidateEmail(string email) => email.Contains("@");
public void SendEmail(MailMessage message) { /* send email */ }
}
Issue: This class handles multiple tasks: email validation, sending emails, and user registration.
Refactored with SRP:
public class UserService
{
private readonly EmailService _emailService;
private readonly DbContext _dbContext;
public UserService(EmailService emailService, DbContext dbContext)
{
_emailService = emailService;
_dbContext = dbContext;
}
public void Register(string email, string password)
{
if (!_emailService.ValidateEmail(email))
throw new ValidationException("Invalid email");
var user = new User(email, password);
_dbContext.Save(user);
_emailService.SendEmail(new MailMessage("myname@mydomain.com", email) { Subject="Hi!" });
}
}
public class EmailService
{
private readonly SmtpClient _smtpClient;
public EmailService(SmtpClient smtpClient) { _smtpClient = smtpClient; }
public bool ValidateEmail(string email) => email.Contains("@");
public void SendEmail(MailMessage message) => _smtpClient.Send(message);
}
Each class now has one responsibility.
O: Open/Closed Principle (OCP)
Definition: "Software entities should be open for extension but closed for modification."
This principle allows us to add new features without changing existing tested code, reducing the risk of introducing bugs.
Example
public abstract class Shape { public abstract double Area(); }
public class Rectangle : Shape { public double Height { get; set; } public double Width { get; set; } public override double Area() => Height * Width; }
public class Circle : Shape { public double Radius { get; set; } public override double Area() => Math.PI * Radius * Radius; }
public class AreaCalculator
{
public double TotalArea(Shape[] shapes)
{
double area = 0;
foreach (var shape in shapes)
area += shape.Area();
return area;
}
}
Adding a new shape now requires no modification to AreaCalculator, only a new class.
L: Liskov Substitution Principle (LSP)
Definition: "Derived classes must be substitutable for their base classes without altering behavior."
This ensures that new subclasses can replace their base class without breaking the program.
Example with Interfaces
public interface IReadableSqlFile { string LoadText(); }
public interface IWritableSqlFile { void SaveText(); }
public class SqlFile : IReadableSqlFile, IWritableSqlFile { /* ... */ }
public class ReadOnlySqlFile : IReadableSqlFile { /* ... */ }
public class SqlFileManager
{
public string GetTextFromFiles(List<IReadableSqlFile> files) { /* ... */ }
public void SaveTextIntoFiles(List<IWritableSqlFile> files) { /* ... */ }
}
Now SqlFileManager works with any class implementing these interfaces without modification.
I: Interface Segregation Principle (ISP)
Definition: "Clients should not be forced to implement interfaces they don't use."
Keep interfaces small and focused.
Example
public interface IProgrammer { void WorkOnTask(); }
public interface ILead { void AssignTask(); void CreateSubTask(); }
public class Manager : ILead { /* ... */ }
public class Programmer : IProgrammer { /* ... */ }
public class TeamLead : IProgrammer, ILead { /* ... */ }
Each role implements only the methods relevant to them.
D: Dependency Inversion Principle (DIP)
Definition: "High-level modules should not depend on low-level modules; both should depend on abstractions."
Decouple high-level classes from low-level implementations using interfaces.
Example
public interface ILogger { void LogMessage(string message); }
public class FileLogger : ILogger { /* ... */ }
public class DbLogger : ILogger { /* ... */ }
public class ExceptionLogger
{
private readonly ILogger _logger;
public ExceptionLogger(ILogger logger) { _logger = logger; }
public void LogException(Exception ex) { _logger.LogMessage(ex.Message); }
}
Now ExceptionLogger is flexible and can log to any destination implementing ILogger.
Conclusion
By applying SOLID principles:
- Your code becomes readable and maintainable
- The system is easier to extend
- Classes remain loosely coupled and focused
Yes, the number of classes may increase, but the benefits of maintainable, clean, and scalable code far outweigh the additional lines of code.
Start implementing SOLID in your C# projects and experience better software design.
