Skip to content
SOLID Principles Chapter 1 of 1

Chapter 1 of 1

Mastering SOLID Principles with Practical Examples in C#

Huy Vũ Sep 24, 2026 5 min read Open original article

Understanding and applying the SOLID principles is essential for any developer aiming to create robust, scalable, and maintainable software. Let’s dive into each principle with practical examples using C# and .NET Core.

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should only have one job or responsibility.

Benefit: By ensuring that a class has only one responsibility, it becomes easier to maintain and understand. It also helps in reducing the risk of unintended consequences when changes are made.

Example: Let’s create a simple example which violates SRP:

C#

public class Report
{
    public string Generate(Params...);
    public void SaveToFile(Params...);
    public void SendByEmail(Params...);
}		

To adhere to SRP, we should split it into several files:

C#

public interface IReportGenerator
{
    string Generate(string data);
}

public interface IFileManager
{
    string Save(string content, string path);
}

public interface IEmailService
{
    bool Send(string recipient, string subject, string body);
}	

2. Open/Closed Principle (OCP)

Definition: Software entities should be open for extension but closed for modification.

Benefit: This principle allows developers to add new functionality without changing existing code, thereby reducing the risk of introducing bugs into the system.

Example: Let’s create a simple example which violates OCP:

C#

public class PaymentProcessor
{
	public void Process(string method)
	{
		if (method = PaymentType.CreditCard) {}
		else if (method = PaymentType.PayPal) {}
	}
}		

Now let’s create a simple payment system where we want to extend the types of payments without modifying existing code.

C#

public interface IPaymentMethod { void Process(); }

public class CreditCardPayment : IPaymentMethod
{
	public void Process() => Console.WriteLine("CreditCard Payment.");
}

public class PayPalPayment : IPaymentMethod
{
	public void Process() => Console.WriteLine("PayPal Payment.");
}		

C#

public class PaymentProcessor
{
	public void Process(IPaymentMethod payment) => payment.Process();
}

We can now add new payment methods by extending the Payment class without modifying existing code.

3. Liskov Substitution Principle (LSP)

Definition: Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

Benefit: Ensures that derived classes extend the base class without changing its behavior, promoting code reusability and robustness.

Example: Let’s consider a bird hierarchy where a subclass violates LSP by not supporting the same behavior as the base class.

C#

public class Bird
{
	public virtual void Fly() => Console.WriteLine("Fly...");
}

public class Sparrow : Bird { }
public class Penguin : Bird
{
	public override void Fly() => throw new InvalidOperationExpection("Penguins don't fly!");
}
		

To adhere to LSP, we can introduce an interface that better represents the capabilities of different birds:

C#

public interface IFlyable
{
	void Fly();
}

public class Sparrow : IFlyable
{
	public void Fly() => Console.WriteLine("I'm flying!");
}

public class Penguin
{
	public void Swin() => Console.WriteLine("I'm swimming!");
}
			

4. Interface Segregation Principle (ISP)

Definition: A client should not be forced to depend on interfaces it does not use.

Benefit: By splitting large interfaces into smaller, more specific ones, you can reduce the complexity of the code and make it more understandable and easier to manage.

Example: Consider an overly large interface that violates ISP:

C#

public interface IWorker
{
	void Work();
	void Eat();
}

public class Robot : IWorker
{
	public void Work() => Console.WriteLine("Robot working.");
	public void Eat() => throw new NotImplementedExcepetion("Robots don't eat!");
}
			

A developer implementing this interface for a robot would be forced to implement methods it doesn’t need.

To adhere to ISP, we can split the interface into more specific ones:

C#

public interface IWorkable
{
	void Work();
}

public interface IEatable
{
	void Eat();
}

public class Human : IWorkable, IEatable
{
	public void Work() => Console.WriteLine("Human working.");
	public void Eat() => Console.WriteLine ("Human eating.");
}

public class Robot : IWorkable
{
	public void Work() => Console.WriteLine("Robot working.");
}
		

5. Dependency Inversion Principle (DIP)

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

Benefit: Promotes decoupling of software components, making the system more flexible and easier to modify or extend.

Example: Consider a high-level class directly depending on a low-level class:

C#

public class EmailService
[
	public void Send(string message) => Console.WriteLine(message);
}

public class OrderService
{
	private readonly EmailService _emailService = new EmailService();
	
	public void PlaceOrder()
	{
		_emailService.Send("Order placed!");
	}
}
			

To adhere to DIP, we introduce an abstraction:

C#

public interface IMessageService
{
	void Send(string message);  // Defining abstraction
}

public class OrderService
{
	private readonly IMessageService _messageService;
	
	public OrderService(IMessageService messageService)
	{
		_messageService = messageService;
	}
	
	public void PlaceOrder()
	{
		_messageService.Send("Order placed!");
	}
}	

Conclusion

By applying these principles, we can create a more modular, scalable, and maintainable codebase. These examples demonstrate how to adhere to each of the SOLID principles using C# and .NET Core, enabling us to write better software.