Introduction to SOLID Principles in C#
\nDeveloping in C# within the .NET ecosystem requires not only knowledge of syntax but also the ability to design flexible, maintainable, and testable systems. This is precisely why the SOLID principles were created — five fundamental rules of object-oriented programming formulated by Robert C. Martin (Uncle Bob).
\nThe SOLID acronym stands for:
\n- \n
- S — Single Responsibility Principle \n
- O — Open/Closed Principle \n
- L — Liskov Substitution Principle \n
- I — Interface Segregation Principle \n
- D — Dependency Inversion Principle \n
In this article, we will break down each principle in practice with examples in C#, show how violating them leads to problems, and how proper application makes code cleaner.
\n\n1. Single Responsibility Principle (SRP)
\nEssence: A class should have only one reason to change. In other words, each class is responsible for one clearly defined task.
\n\nViolation of SRP in C#
\nConsider a typical example — a class OrderService that does too much:
public class OrderService\n{\n public void ProcessOrder(Order order)\n {\n // 1. Save to DB\n SaveToDatabase(order);\n \n // 2. Send email\n SendEmail(order.CustomerEmail, "Order placed");\n \n // 3. Logging\n File.WriteAllText("log.txt", $"Order {order.Id} processed");\n }\n \n private void SaveToDatabase(Order order) { /* ... */ }\n private void SendEmail(string email, string message) { /* ... */ }\n}\nProblem: Changing the email sending logic or the logging method will lead to changes in the OrderService class. This violates SRP.
Correct SRP Implementation
\nSeparate responsibilities into individual services:
\npublic class OrderProcessor\n{\n private readonly IOrderRepository _repository;\n private readonly INotificationService _notificationService;\n private readonly ILogger _logger;\n \n public OrderProcessor(\n IOrderRepository repository,\n INotificationService notificationService,\n ILogger logger)\n {\n _repository = repository;\n _notificationService = notificationService;\n _logger = logger;\n }\n \n public void ProcessOrder(Order order)\n {\n _repository.Save(order);\n _notificationService.SendConfirmation(order.CustomerEmail);\n _logger.Log($"Order {order.Id} processed");\n }\n}\n\npublic interface IOrderRepository { void Save(Order order); }\npublic interface INotificationService { void SendConfirmation(string email); }\npublic interface ILogger { void Log(string message); }\nNow each class is responsible only for its own task. Changing the logging logic does not affect order processing.
\n\n2. Open/Closed Principle (OCP)
\nEssence: Classes should be open for extension but closed for modification. That is, you should be able to add new behavior without changing existing code.
\n\nViolation of OCP in C#
\nA classic example — a calculator with conditions:
\npublic class AreaCalculator\n{\n public double CalculateArea(object shape)\n {\n if (shape is Rectangle rect)\n return rect.Width * rect.Height;\n else if (shape is Circle circle)\n return Math.PI * circle.Radius * circle.Radius;\n // Adding a new shape requires changing this method!\n throw new NotSupportedException();\n }\n}