Skip to content

SOLID Principles in C#

SOLID is a set of five design principles that help you write object-oriented code that is easy to understand, easy to change and easy to test. They are used every day in professional .NET teams, they shape how ASP.NET Core itself is built, and they are asked about in almost every C# interview for developers with 2+ years of experience.

The principles were collected by Robert C. Martin ("Uncle Bob") around 2000, and the acronym SOLID was later coined by Michael Feathers.

The five principles at a glance

LetterPrincipleIn one simple lineTypical ASP.NET Core example
SSingle ResponsibilityA class should have only one reason to change.Separate validator, repository and email service instead of one giant OrderService.
OOpen/ClosedAdd new features by adding new code, not by editing working code.Add a new payment gateway as a new class, without touching the switch.
LLiskov SubstitutionA child class must work anywhere its parent is expected.A payment type that can't be refunded must not pretend to support Refund().
IInterface SegregationMany small, focused interfaces are better than one fat interface.Split IProductRepository into IProductReader and IProductWriter.
DDependency InversionDepend on interfaces, not on concrete classes.Controllers receive IOrderRepository through constructor injection.

Think of a well-run restaurant. The chef cooks, the cashier bills, the waiter serves (S). A new dish is added to the menu without rebuilding the kitchen (O). Any trained chef can replace another chef and the food is still correct (L). The waiter is not forced to learn accounting (I). The restaurant depends on "a chef", not on one particular person (D).

Why do we need SOLID?

Every real project changes: new payment methods, new reports, a new database, a new notification channel. Code that ignores design principles shows four classic symptoms as it grows:

  • Rigid: a small change forces changes in many other places.
  • Fragile: fixing one bug breaks something unrelated.
  • Immobile: you can't reuse a piece of code in another project because it is tied to everything else.
  • Hard to test: you can't unit test a class without a real database, real SMTP server or real payment gateway.

SOLID directly attacks these problems.

A typical "before SOLID" class

Here is a controller you will see in many real projects. It works, but it breaks every SOLID principle:

public class OrderController : Controller
{
    [HttpPost]
    public IActionResult PlaceOrder(OrderDto dto)
    {
        // 1. Validation
        if (dto.Items.Count == 0) return BadRequest("Cart is empty");

        // 2. Price calculation with discounts
        decimal total = dto.Items.Sum(i => i.Price * i.Quantity);
        if (dto.CustomerType == "Gold") total *= 0.90m;
        else if (dto.CustomerType == "Silver") total *= 0.95m;

        // 3. Payment, with one if-else per gateway
        if (dto.PaymentMode == "UPI") { /* call UPI API */ }
        else if (dto.PaymentMode == "Card") { /* call card API */ }

        // 4. Saving with hard-coded ADO.NET
        using var con = new SqlConnection("Server=.;Database=Shop;Trusted_Connection=True;");
        // ... INSERT INTO Orders ...

        // 5. Sending an email with hard-coded SMTP
        var smtp = new SmtpClient("smtp.company.com");
        smtp.Send("noreply@company.com", dto.Email, "Order placed", $"Total: {total}");

        return Ok();
    }
}
  • S is broken: one method validates, calculates, pays, saves and emails. It has five reasons to change.
  • O is broken: every new customer type or payment mode means editing this method.
  • D is broken: it creates SqlConnection and SmtpClient itself, so it can't be unit tested.
  • L and I problems appear as soon as you try to create a "general" payment or repository interface for all of these cases.

In the next five lessons we take this same online shop (orders, payments, products, notifications) and fix it step by step, one principle at a time.

SOLID is already inside ASP.NET Core

  • Built-in dependency injection (builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>()) is the Dependency Inversion Principle in action.
  • Middleware lets you add features (logging, authentication, exception handling) to the request pipeline without modifying existing middleware: Open/Closed.
  • Small interfaces such as ILogger<T>, IOptions<T>, IMemoryCache and IHttpClientFactory follow Interface Segregation.
  • Filters, model binders and validators each do one job: Single Responsibility.

A word of balance

SOLID is a set of guidelines, not laws. Applying them blindly can create dozens of tiny interfaces and classes nobody needs ("over-engineering"). A good rule used in industry: apply a principle when you feel the pain it solves, or when you clearly know that part of the code will change or needs to be tested.

What you will learn in this series

  1. Single Responsibility Principle: splitting a "god class" order service.
  2. Open/Closed Principle: adding payment gateways without editing existing code.
  3. Liskov Substitution Principle: designing payment and storage types that don't surprise callers.
  4. Interface Segregation Principle: read and write repositories.
  5. Dependency Inversion Principle: interfaces, DI in Program.cs and unit tests with Moq.
  6. SOLID interview questions and answers.

Summary

  • SOLID = Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion.
  • Its goal is code that is easy to change, extend, reuse and test.
  • ASP.NET Core is designed around these principles, especially dependency injection.
  • Use SOLID with common sense; avoid creating abstractions you don't need.