SOLID Principles in C# — Complete Guide
In this tutorial, you will learn about SOLID Principles in C#. We cover key concepts, practical examples, and best practices to help you master this topic.
Hook
SOLID is the foundation of maintainable object-oriented design. These five principles guide you in creating code that is easy to understand, extend, and refactor. C# and .NET provide features that support each principle, from interfaces to Dependency Injection. Mastering SOLID transforms good code into great architecture.
Learning Path
graph LR A[SOLID] --> B[SRP] A --> C[OCP] A --> D[LSP] A --> E[ISP] A --> F[DIP] B --> G[Single Responsibility] C --> H[Open/Closed] D --> I[Liskov Substitution] E --> J[Interface Segregation] F --> K[Dependency Inversion] style A fill:#4a90d9,color:#fff style B fill:#4a90d9,color:#fff style C fill:#4a90d9,color:#fff style D fill:#4a90d9,color:#fff style E fill:#4a90d9,color:#fff style F fill:#4a90d9,color:#fff
S: Single Responsibility Principle (SRP)
A class should have only one reason to change.
// BAD: Report class handles data, formatting, and persistence
public class BadReport
{
public string Title { get; set; }
public List<string> Data { get; set; }
public void GenerateHtmlReport() { /* HTML formatting */ }
public void GeneratePdfReport() { /* PDF formatting */ }
public void SaveToDatabase() { /* Database logic */ }
public void SendByEmail() { /* Email logic */ }
}
// GOOD: Each class has a single responsibility
public class Report
{
public string Title { get; set; }
public List<string> Data { get; set; }
}
public class ReportFormatter
{
public string ToHtml(Report report) => $"<h1>{report.Title}</h1>";
public string ToPdf(Report report) => $"PDF:{report.Title}";
}
public class ReportRepository
{
public void Save(Report report) => Console.WriteLine("Saving to DB");
}
public class EmailService
{
public void SendReport(string to, Report report) =>
Console.WriteLine($"Emailing report to {to}");
}
O: Open/Closed Principle (OCP)
Classes should be open for extension but closed for modification.
// BAD: Adding a new shape requires modifying the calculator
public class BadAreaCalculator
{
public double CalculateArea(object shape)
{
if (shape is Circle c)
return Math.PI * c.Radius * c.Radius;
if (shape is Rectangle r)
return r.Width * r.Height;
// Must modify this method to add new shapes!
throw new NotSupportedException();
}
}
// GOOD: Extend behavior through inheritance
public abstract class Shape
{
public abstract double CalculateArea();
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double CalculateArea() =>
Math.PI * Radius * Radius;
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double CalculateArea() => Width * Height;
}
// New shape: no modification needed
public class Triangle : Shape
{
public double Base { get; set; }
public double Height { get; set; }
public override double CalculateArea() => 0.5 * Base * Height;
}
public class AreaCalculator
{
public double CalculateTotalArea(Shape[] shapes) =>
shapes.Sum(s => s.CalculateArea());
}
L: Liskov Substitution Principle (LSP)
Derived classes must be substitutable for their base classes.
// BAD: Square violates LSP when inheriting from Rectangle
public class BadRectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area => Width * Height;
}
public class BadSquare : BadRectangle
{
public override int Width
{
set { base.Width = value; base.Height = value; }
}
public override int Height
{
set { base.Width = value; base.Height = value; }
}
}
// Client assumes Rectangle behavior
void ResizeToFit(BadRectangle rect)
{
rect.Width = 5;
rect.Height = 10;
// Client expects Width=5, Height=10, Area=50
// But with BadSquare: Width=10, Height=10, Area=100!
}
// GOOD: Use abstraction that both can implement
public interface IShape
{
int Area { get; }
}
public readonly struct LspRectangle : IShape
{
public int Width { get; }
public int Height { get; }
public int Area => Width * Height;
public LspRectangle(int width, int height)
{
Width = width;
Height = height;
}
}
public readonly struct LspSquare : IShape
{
public int Side { get; }
public int Area => Side * Side;
public LspSquare(int side) => Side = side;
}
I: Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
// BAD: Fat interface forces all workers to implement irrelevant methods
public interface IWorker
{
void Work();
void Eat();
void Sleep();
}
public class HumanWorker : IWorker
{
public void Work() => Console.WriteLine("Working");
public void Eat() => Console.WriteLine("Eating");
public void Sleep() => Console.WriteLine("Sleeping");
}
public class RobotWorker : IWorker
{
public void Work() => Console.WriteLine("Working");
public void Eat() => throw new NotSupportedException(); // Robots don't eat!
public void Sleep() => throw new NotSupportedException(); // Robots don't sleep!
}
// GOOD: Segregated interfaces
public interface IWorkable
{
void Work();
}
public interface IFeedable
{
void Eat();
}
public interface ISleepable
{
void Sleep();
}
public class GoodHumanWorker : IWorkable, IFeedable, ISleepable
{
public void Work() => Console.WriteLine("Working");
public void Eat() => Console.WriteLine("Eating");
public void Sleep() => Console.WriteLine("Sleeping");
}
public class GoodRobotWorker : IWorkable
{
public void Work() => Console.WriteLine("Working");
}
D: Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
// BAD: High-level depends directly on low-level
public class BadOrderService
{
private readonly SqlServerDatabase _database;
public BadOrderService()
{
_database = new SqlServerDatabase(); // Tight coupling
}
public void PlaceOrder(Order order)
{
_database.SaveOrder(order);
}
}
public class SqlServerDatabase
{
public void SaveOrder(Order order) =>
Console.WriteLine("Saving to SQL Server");
}
// GOOD: Both depend on abstraction
public interface IOrderRepository
{
void Save(Order order);
}
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly INotificationService _notification;
public OrderService(IOrderRepository repository,
INotificationService notification)
{
_repository = repository;
_notification = notification;
}
public async Task PlaceOrder(Order order)
{
_repository.Save(order);
await _notification.SendAsync("order@system.com", "New order placed");
}
}
public class SqlServerOrderRepository : IOrderRepository
{
public void Save(Order order) =>
Console.WriteLine("Saving to SQL Server");
}
public class MongoDbOrderRepository : IOrderRepository
{
public void Save(Order order) =>
Console.WriteLine("Saving to MongoDB");
}
// Configuration decides implementation
// services.AddScoped<IOrderRepository, SqlServerOrderRepository>();
Applying SOLID Together
A practical example combining all principles.
// SRP: Each class has one job
public interface IEmployeeRepository
{
Task<Employee> GetByIdAsync(int id);
}
public interface IPayrollCalculator
{
decimal CalculatePay(Employee employee);
}
public interface IPaymentProcessor
{
Task ProcessPaymentAsync(Employee employee, decimal amount);
}
// OCP: New pay types via extension
public abstract class PayCalculator
{
public abstract decimal Calculate(Employee employee);
}
public class SalaryCalculator : PayCalculator
{
public override decimal Calculate(Employee e) => e.AnnualSalary / 12;
}
public class HourlyCalculator : PayCalculator
{
public override decimal Calculate(Employee e) => e.HoursWorked * e.HourlyRate;
}
// LSP: Substitutable calculators
public class PayrollService
{
private readonly IEmployeeRepository _repo;
private readonly PayCalculator _calculator;
private readonly IPaymentProcessor _processor;
public PayrollService(IEmployeeRepository repo, PayCalculator calculator,
IPaymentProcessor processor)
{
_repo = repo;
_calculator = calculator;
_processor = processor;
}
public async Task ProcessPayroll(int employeeId)
{
var employee = await _repo.GetByIdAsync(employeeId);
var pay = _calculator.Calculate(employee);
await _processor.ProcessPaymentAsync(employee, pay);
}
}
// ISP: Segregated reporting interfaces
public interface IBasicReport { string Generate(); }
public interface IDetailedReport { string GenerateDetails(); }
public interface IExportableReport { Task ExportAsync(string path); }
// DIP: All dependencies injected through constructors
// Entire system is testable and flexible
Common Mistakes
SRP taken too far: Over-splitting creates many tiny classes. Find the right granularity for your context.
OCP with premature abstraction: Do not abstract for hypothetical future requirements. Apply OCP when you actually need to extend.
LSP violations with inheritance hierarchies: Favor Composition Over Inheritance. Use interfaces instead of base classes when possible.
ISP ignored in Api Design: Large interfaces create coupling. Design small, focused interfaces that represent specific capabilities.
DIP without DI container: DIP requires constructor injection. Using
newinside classes creates hard dependencies.
Practice Questions
Refactor a class that handles both database operations and email notifications to follow SRP.
Design a plugin system using OCP where new file format handlers can be added without modifying existing code.
Identify and fix LSP violations in a class hierarchy of
Bird,FlyingBird, andPenguin.Challenge: Build a complete SOLID-compliant order processing pipeline with validation, pricing, inventory, payment, and notification.
FAQ
Mini Project: SOLID Payroll System
Build a complete payroll processing system that adheres to SOLID.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
// SRP: Separate concerns
public record Employee(int Id, string Name, decimal AnnualSalary,
decimal HoursWorked, decimal HourlyRate, EmployeeType Type);
public enum EmployeeType { Salary, Hourly }
// OCP: Open for extension
public interface IPayStrategy
{
decimal Calculate(Employee employee);
}
public class SalaryPayStrategy : IPayStrategy
{
public decimal Calculate(Employee e) => Math.Round(e.AnnualSalary / 12, 2);
}
public class HourlyPayStrategy : IPayStrategy
{
public decimal Calculate(Employee e) =>
Math.Round(e.HoursWorked * e.HourlyRate, 2);
}
public class PayStrategyFactory
{
private static readonly Dictionary<EmployeeType, IPayStrategy> Strategies = new()
{
[EmployeeType.Salary] = new SalaryPayStrategy(),
[EmployeeType.Hourly] = new HourlyPayStrategy()
};
public IPayStrategy GetStrategy(EmployeeType type) =>
Strategies.TryGetValue(type, out var strategy)
? strategy
: throw new ArgumentException($"Unknown type: {type}");
}
// ISP: Segregated interfaces
public interface IEmployeeRepository
{
Task<List<Employee>> GetAllAsync();
}
public interface IPaymentRepository
{
Task SavePaymentAsync(int employeeId, decimal amount, DateTime date);
}
public interface INotificationService
{
Task NotifyAsync(string recipient, string message);
}
// DIP: Depend on abstractions
public class PayrollProcessor
{
private readonly IEmployeeRepository _repo;
private readonly IPaymentRepository _payments;
private readonly INotificationService _notifications;
private readonly PayStrategyFactory _strategyFactory;
public PayrollProcessor(
IEmployeeRepository repo,
IPaymentRepository payments,
INotificationService notifications,
PayStrategyFactory strategyFactory)
{
_repo = repo;
_payments = payments;
_notifications = notifications;
_strategyFactory = strategyFactory;
}
public async Task ProcessMonthlyPayroll()
{
var employees = await _repo.GetAllAsync();
foreach (var employee in employees)
{
var strategy = _strategyFactory.GetStrategy(employee.Type);
var pay = strategy.Calculate(employee);
await _payments.SavePaymentAsync(employee.Id, pay, DateTime.UtcNow);
await _notifications.NotifyAsync(employee.Name,
$"Your monthly pay of ${pay} has been processed.");
}
}
}
// Concrete implementations
public class InMemoryEmployeeRepo : IEmployeeRepository
{
public Task<List<Employee>> GetAllAsync() => Task.FromResult(new List<Employee>
{
new(1, "Alice", 120000, 0, 0, EmployeeType.Salary),
new(2, "Bob", 0, 80, 50, EmployeeType.Hourly)
});
}
public class ConsolePaymentRepo : IPaymentRepository
{
public Task SavePaymentAsync(int id, decimal amount, DateTime date)
{
Console.WriteLine($"Saved payment: Employee {id}, ${amount}, {date:d}");
return Task.CompletedTask;
}
}
public class ConsoleNotification : INotificationService
{
public Task NotifyAsync(string recipient, string message)
{
Console.WriteLine($"NOTIFY {recipient}: {message}");
return Task.CompletedTask;
}
}
// Usage
var processor = new PayrollProcessor(
new InMemoryEmployeeRepo(),
new ConsolePaymentRepo(),
new ConsoleNotification(),
new PayStrategyFactory());
await processor.ProcessMonthlyPayroll();
Output:
Saved payment: Employee 1, $10000.00, 6/28/2026
NOTIFY Alice: Your monthly pay of $10000.00 has been processed.
Saved payment: Employee 2, $4000.00, 6/28/2026
NOTIFY Bob: Your monthly pay of $4000.00 has been processed.
SOLID principles transform C# code from fragile, tightly-coupled systems into flexible, maintainable architectures. Combined with .NET features like dependency injection and interfaces, these five principles will guide you in building professional-grade software.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro