Introduction: Why Do You Need SOLID Principles in C#?
Every C# developer sooner or later faces the problem of "dirty code": classes that do everything at once, unreadable dependencies, and the inability to make changes without risking breaking the entire project. SOLID is an acronym for five fundamental principles of object-oriented design formulated by Robert Martin. They help create flexible, maintainable, and testable code in C# (.NET).
In this article, we will break down each SOLID principle with a theoretical foundation and practical examples in C#. You will learn how to apply them in real projects while avoiding common mistakes.
1. Single Responsibility Principle (SRP)
Theory
A class should have only one reason to change. In other words, each class is responsible for only one part of the program's functionality. Violating SRP leads to changes in one operation (e.g., saving to a database) affecting calculation or display logic.
Practice in C#
Consider a typical example of SRP violation — a class that simultaneously calculates salary and saves an employee to the database:
// ❌ SRP Violation: the class does too muchpublic class Employee{ public string Name { get; set; } public decimal Salary { get; set; }
public decimal CalculateBonus() { return Salary * 0.1m; }
public void SaveToDatabase() { // Database saving logic Console.WriteLine("Employee saved to database"); }}Let's fix the situation by separating responsibilities:
// ✅ SRP: each class is responsible for its own taskpublic class Employee{ public string Name { get; set; } public decimal Salary { get; set; }}
public class BonusCalculator{ public decimal Calculate(Employee employee) { return employee.Salary * 0.1m; }}
public class EmployeeRepository{ public void Save(Employee employee) { Console.WriteLine("Employee saved to database"); }}2. Open-Closed Principle (OCP)
Theory
Software entities (classes, modules, functions) should be open for extension but closed for modification. This means you can add new functionality without changing existing code.
Practice in C#
Suppose we need to calculate the area of different shapes. Violating OCP means using if-else or switch for each new shape type:
// ❌ OCP Violation: adding a new shape requires changing the codepublic class AreaCalculator{ public double Calculate(object shape) { if (shape is Circle circle) return Math.PI * circle.Radius * circle.Radius; else if (shape is Rectangle rect) return rect.Width * rect.Height; // When adding Triangle, you would need to add another if return 0; }}The solution is to use abstraction and polymorphism:
// ✅ OCP: extend through inheritance without touching existing codepublic abstract class Shape{ public abstract double GetArea();}
public class Circle : Shape{ public double Radius { get; set; } public override double GetArea() => Math.PI * Radius * Radius;}
public class Rectangle : Shape{ public double Width { get; set; } public double Height { get; set; } public override double GetArea() => Width * Height;}
public class AreaCalculator{ public double Calculate(Shape shape) { return shape.GetArea(); // Works for any shape }}3. Liskov Substitution Principle (LSP)
Theory
Objects of a base class should be replaceable with objects of its subclasses without breaking