SOLID Principles in C#: Advanced Application and Practical Cases

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: When SOLID Ceases to Be Theory

Most developers are familiar with the SOLID acronym at the level of "a class should be responsible for one thing." However, in real-world C# (.NET) projects with Dependency Injection, microservices, and asynchronous patterns, naive adherence to the principles leads to the opposite effect — overcomplication, interface bloat, and violation of encapsulation.

In this article, we will look at SOLID not as a set of dogmas, but as a tool for making architectural decisions. You will learn how to correctly apply the principles in advanced scenarios: working with IoC containers, composition over inheritance, error handling through interfaces, and saga testing.



1. Single Responsibility: Boundaries of Responsibility in DDD

The Single Responsibility Principle (SRP) is often confused with "one class — one function." In fact, it refers to the reason to change. In the context of C# and Domain-Driven Design, this means that a class should change for only one reason, related to a single actor or business rule.



Advanced Antipattern: "The God Entity"

Consider a typical example of SRP violation in .NET — the OrderEntity class, which simultaneously manages the order state, calculates taxes, sends emails, and saves data to the database via Entity Framework.



// ❌ SRP Violationpublic class OrderEntity{    public int Id { get; set; }    public decimal Total { get; set; }    public string Email { get; set; }

public void CalculateTax() { /* tax logic */ } public void SendConfirmation() { /* sending email */ } public void SaveToDatabase() { /* EF Core SaveChanges */ }}

Problem: a change in the tax business rule (e.g., a new law) will lead to a change in the class that is also responsible for sending notifications. This increases the risk of regression.



Solution: Extracting Services via DI

// ✅ SRP via Servicespublic class Order{    public int Id { get; set; }    public decimal Total { get; set; }    // State only, no logic}

public class TaxCalculator : ITaxCalculator{ public decimal Calculate(Order order) { /* ... */ }}

public class EmailService : INotificationService{ public Task SendConfirmation(Order order) { /* ... */ }}

public class OrderRepository : IRepository<Order>{ public Task SaveAsync(Order order) { /* EF Core */ }}

Now each class has exactly one reason to change. The DI container (Microsoft.Extensions.DependencyInjection) wires them together via constructors.



2. Open/Closed: Extension via Strategy and Decorator

The Open/Closed Principle (OCP) states: a class should be open for extension but closed for modification. In .NET, this is achieved not only through inheritance but also through composition with the Strategy and Decorator patterns.



Advanced Case: Validation with Growing Rules

Imagine you have an order validation system. The initial version only checked the total and address. A month later, a rule "cannot order items with a total exceeding the limit" was added. Instead of modifying the OrderValidator class, use composition.



// ❌ OCP Violation — modifying an existing classpublic class OrderValidator{    public bool Validate(Order order)    {        if (order.Total < 0) return false;        if (string.IsNullOrEmpty(order.Address)) return false;        // New rule — modifying the class        if (order.Total > 10000) return false;        return true;    }}


// ✅ OCP via Validator Compositionpublic interface IValidator<T>{    bool Validate(T entity);}

public class TotalValidator : IValidator<Order>{ public bool Validate(Order order) => order.Total > 0;}

pu

Blogs

Book Recommendations