Introduction: Why SOLID is the Foundation of Quality C# Code
Developing in C# within the .NET ecosystem requires not only knowledge of syntax but also the ability to design flexible, maintainable, and testable applications. SOLID is an acronym for five fundamental principles of object-oriented design formulated by Robert Martin. They help developers avoid "rigid" dependencies, code duplication, and scalability issues.
In this article, we will break down each of the five SOLID principles as applied to C#: from theory to real code examples. You will learn how refactoring with SOLID in mind makes your code cleaner and project maintenance easier.
1. Single Responsibility Principle (SRP)
Theory
A class should have only one reason to change. In other words, each class is responsible for exactly one functionality or business logic. If a class does too much, it becomes difficult to test, modify, and reuse.
Practice: SRP violation
public class OrderProcessor{ public void ProcessOrder(Order order) { // 1. Order validation if (order.Total <= 0) throw new Exception("Invalid order"); // 2. Save to database using var db = new AppDbContext(); db.Orders.Add(order); db.SaveChanges(); // 3. Send email notification SmtpClient client = new SmtpClient(); client.Send("admin@shop.com", order.CustomerEmail, "Order confirmed", "Thank you!"); }}Here, OrderProcessor violates SRP: it is responsible for validation, database operations, and sending emails. Changing the notification logic would require modifying this same class.
Refactoring with SRP
public class OrderValidator{ public bool Validate(Order order) => order.Total > 0;}
public class OrderRepository{ public void Save(Order order) { using var db = new AppDbContext(); db.Orders.Add(order); db.SaveChanges(); }}
public class EmailService{ public void SendConfirmation(string email) { SmtpClient client = new SmtpClient(); client.Send("admin@shop.com", email, "Order confirmed", "Thank you!"); }}
public class OrderProcessor{ private readonly OrderValidator _validator; private readonly OrderRepository _repository; private readonly EmailService _emailService;
public OrderProcessor(OrderValidator validator, OrderRepository repository, EmailService emailService) { _validator = validator; _repository = repository; _emailService = emailService; }
public void ProcessOrder(Order order) { if (!_validator.Validate(order)) throw new Exception("Invalid order"); _repository.Save(order); _emailService.SendConfirmation(order.CustomerEmail); }}Now each class has a single area of responsibility. This simplifies testing and modification.
2. Open/Closed Principle (OCP)
Theory
Software entities (classes, modules, functions) should be open for extension but closed for modification. That is, you should be able to add new functionality without changing existing code.
Practice: OCP violation
public class ShapeDrawer{ public void DrawShape(object shape) { if (shape is Circle circle) Console.WriteLine($"Drawing circle with radius {circle.Radius}"); else if (shape is Rectangle rect) Console.WriteLine($"Drawing rectangle {rect.Width}x{rect.Height}"); // When adding a new shape type — this method must be modified! }}Refactoring with OCP using abstraction
public abstract c