.Net, Azure, C#, Cloud Design Patterns, Nutshell Series, Technology and tricks

SOLID Principles (with c# examples) – Nutshell

SOLID Principles in .NET – Explained with Examples

SOLID Principles in .NET – Explained with Examples

The SOLID principles are five guidelines that help developers write clean, maintainable, and extensible object-oriented code.
Below we go through each principle with concise C# examples showing what is bad and what is good.


1. Single Responsibility Principle (SRP)

Rule: A class should have only one reason to change.

Bad Example

public class User
{
    public string Name { get; set; }
    public string Email { get; set; }

    public void SendEmail(string message)
    {
        // Logic to send email
    }
}

Here, User manages both data and email-sending logic. It has more than one responsibility.

Good Example

public class User
{
    public string Name { get; set; }
    public string Email { get; set; }
}

public class EmailService
{
    public void SendEmail(string email, string message)
    {
        // Logic to send email
    }
}

User now manages only user data, while EmailService handles emails. Each has one responsibility.


2. Open/Closed Principle (OCP)

Rule: Classes should be open for extension but closed for modification.

Bad Example

public class AreaCalculator
{
    public double CalculateArea(object shape)
    {
        if (shape is Circle c)
            return Math.PI * c.Radius * c.Radius;
        if (shape is Square s)
            return s.Side * s.Side;
        return 0;
    }
}

Adding a new shape forces us to modify AreaCalculator, violating OCP.

Good Example

public abstract class Shape
{
    public abstract double Area();
}

public class Circle : Shape
{
    public double Radius { get; set; }
    public override double Area() => Math.PI * Radius * Radius;
}

public class Square : Shape
{
    public double Side { get; set; }
    public override double Area() => Side * Side;
}

Each shape implements its own Area. Adding new shapes doesn’t require changing existing code.


3. Liskov Substitution Principle (LSP)

Rule: Derived classes should be substitutable for their base types without breaking behavior.

Bad Example

public class Bird
{
    public virtual void Fly() { }
}

public class Ostrich : Bird
{
    public override void Fly()
    {
        throw new NotImplementedException();
    }
}

Substituting Ostrich for Bird breaks behavior, since ostriches cannot fly.

Good Example

public abstract class Shape
{
    public abstract double Area();
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    public override double Area() => Width * Height;
}

Rectangle correctly substitutes Shape and can be used anywhere Shape is expected.


4. Interface Segregation Principle (ISP)

Rule: Don’t force classes to implement methods they don’t use.

Bad Example

public interface IMachine
{
    void Print();
    void Scan();
}

public class SimplePrinter : IMachine
{
    public void Print() { }
    public void Scan() { throw new NotImplementedException(); }
}

SimplePrinter doesn’t support scanning, yet it is forced to implement it.

Good Example

public interface IPrinter
{
    void Print();
}

public interface IScanner
{
    void Scan();
}

public class MultiFunctionPrinter : IPrinter, IScanner
{
    public void Print()
    {
        Console.WriteLine("Printing document...");
    }

    public void Scan()
    {
        Console.WriteLine("Scanning document...");
    }
}

Now classes only implement what they need. A simple printer just implements IPrinter.


5. Dependency Inversion Principle (DIP)

Rule: High-level modules should not depend on low-level modules. Both should depend on abstractions.

Bad Example

public class DataManager
{
    private readonly SQLDatabase _db = new SQLDatabase();

    public void Save(string data)
    {
        _db.SaveData(data);
    }
}

public class SQLDatabase
{
    public void SaveData(string data) { }
}

DataManager is tightly coupled to SQLDatabase, making it hard to switch databases.

Good Example

public interface IDatabase
{
    void SaveData(string data);
}

public class SQLDatabase : IDatabase
{
    public void SaveData(string data)
    {
        // Save data to SQL database
    }
}

public class DataManager
{
    private readonly IDatabase _database;
    public DataManager(IDatabase database)
    {
        _database = database;
    }

    public void Save(string data)
    {
        _database.SaveData(data);
    }
}

DataManager now depends on IDatabase. Any database implementation can be injected.


Key Takeaways

  • SRP — One class, one responsibility.
  • OCP — Extend, don’t modify working code.
  • LSP — Subtypes must preserve base-class behavior.
  • ISP — Keep interfaces small and focused.
  • DIP — Depend on abstractions, not concretions.