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.

You must be logged in to post a comment.