Solid Principles In C# Complete Guide

 Last Update:2025-06-23T00:00:00     .NET School AI Teacher - SELECT ANY TEXT TO EXPLANATION.    8 mins read      Difficulty-Level: beginner

Understanding the Core Concepts of SOLID Principles in C#

SOLID Principles in C# Explained in Detail

1. Single Responsibility Principle (SRP): According to the Single Responsibility Principle, a class should have only one reason to change, meaning it should have only one job. This ensures that the class remains focused and its purpose remains clear. For example, if a class is responsible for managing a user’s database actions, it should not also be responsible for logging. This can be achieved by creating different classes for logging and database actions.

C# Example (SRP):

public class UserRepository
{
    public void Add(User user)
    {
        // Database save logic
    }
}

public class Logger
{
    public void Log(string message)
    {
        // Logging logic
    }
}

2. Open/Closed Principle (OCP): The Open/Closed Principle states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that a class should be designed in a way that it allows its behavior to be extended without modifying its source code. One way to achieve this in C# is through inheritance and polymorphism.

C# Example (OCP):

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

public class Circle : Shape
{
    private double _radius;

    public Circle(double radius)
    {
        _radius = radius;
    }

    public override double CalculateArea()
    {
        return Math.PI * _radius * _radius;
    }
}

public class Square : Shape
{
    private double _side;

    public Square(double side)
    {
        _side = side;
    }

    public override double CalculateArea()
    {
        return _side * _side;
    }
}

3. Liskov Substitution Principle (LSP): Liskov Substitution Principle states that objects of a superclass shall be replaceable with objects of its subclasses without affecting the correctness of the program. In other words, classes and interfaces should be designed in a way that the objects derived from them should be able to be used interchangeably without causing issues. The primary focus of LSP is on method behaviors and their relationships.

C# Example (LSP):

public interface IBird
{
    void Fly();
}

public class Sparrow : IBird
{
    public void Fly()
    {
        Console.WriteLine("Sparrow flying...");
    }
}

public class Ostrich : IBird
{
    public void Fly()
    {
        throw new Exception("Ostriches cannot fly");
    }
}

In the above example, the Ostrich class does not align well with the Liskov Substitution Principle as it does not appropriately implement the Fly() method. A better design might involve rethinking the interface and perhaps introducing a new FlightlessBird interface.

4. Interface Segregation Principle (ISP): ISP states that no client should be forced to depend on methods it does not use. It's about creating small interfaces that are very specific to the needs of the clients, instead of creating one big interface. This allows updating one client without impacting another client that depends on a different method of the old interface.

C# Example (ISP):

public interface IPrintable
{
    void Print(string text);
}

public interface IScannable
{
    string Scan();
}

public class Printer : IPrintable
{
    public void Print(string text)
    {
        Console.WriteLine(text);
    }
}

public class Scanner : IScannable
{
    public string Scan()
    {
        // Scanning logic
        return "Scanned text";
    }
}

5. Dependency Inversion Principle (DIP): Dependency Inversion Principle states that:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend upon details. Details should depend upon abstractions.

This principle aims to reduce the dependencies between classes by introducing abstractions that can help in achieving loose-coupling.

C# Example (DIP):

public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

public class FileLogger : ILogger
{
    public void Log(string message)
    {
        // File logging logic
    }
}

public class LoggingService
{
    private readonly ILogger _logger;

    public LoggingService(ILogger logger)
    {
        _logger = logger;
    }

    public void DoWork()
    {
        _logger.Log("Doing some work");
    }
}

In this example, the LoggingService class depends on the abstraction ILogger rather than a concrete implementation like ConsoleLogger or FileLogger. This design allows for easy switchability between different logging mechanisms without changing the LoggingService implementation.

Conclusion

Online Code run

🔔 Note: Select your programming language to check or run code at

💻 Run Code Compiler

Step-by-Step Guide: How to Implement SOLID Principles in C#

1. Single Responsibility Principle (SRP)

This principle states that a class should have only one reason to change.

Before Applying SRP:

public class Employee
{
    public void CalculatePay()
    {
        // Calculation logic here
    }

    public void SaveToDatabase()
    {
        // Database save logic here
    }

    public void GenerateReport()
    {
        // Reporting logic here
    }
}

After Applying SRP:

public class EmployeePayCalculator
{
    public void CalculatePay()
    {
        // Calculation logic here
    }
}

public class EmployeeDatabase
{
    public void SaveToDatabase()
    {
        // Database save logic here
    }
}

public class EmployeeReporter
{
    public void GenerateReport()
    {
        // Reporting logic here
    }
}

2. Open/Closed Principle (OCP)

This principle states that software entities should be open for extension but closed for modification.

Before Applying OCP:

public class Shape
{
    public string ShapeType { get; set; }

    public double CalculateArea()
    {
        if (this.ShapeType == "Circle")
        {
            // Calculate circle area
        }
        else if (this.ShapeType == "Rectangle")
        {
            // Calculate rectangle area
        }
        // Further shapes could require code changes here
        return 0;
    }
}

After Applying OCP:

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

public class Circle : Shape
{
    public double Radius { get; set; }
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    public override double CalculateArea()
    {
        return Width * Height;
    }
}

// Adding a new shape won't require changes in existing classes
public class Triangle : Shape
{
    public double Base { get; set; }
    public double Height { get; set; }
    public override double CalculateArea()
    {
        return 0.5 * Base * Height;
    }
}

3. Liskov Substitution Principle (LSP)

This principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

Before Applying LSP:

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

public class Penguin : Bird
{
    public override void Fly()
    {
        // Penguins cannot fly
        throw new NotImplementedException("Penguins cannot fly");
    }
}

After Applying LSP:

public abstract class Bird
{
    // No need for the Fly method in the base class
}

public abstract class FlyingBird : Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("Flying...");
    }
}

public class Sparrow : FlyingBird
{
    public override void Fly()
    {
        Console.WriteLine("Sparrow flying...");
    }
}

public class Penguin : Bird
{
    // Penguins can have their own methods for swimming or doing other activities
    public void Swim()
    {
        Console.WriteLine("Penguin swimming...");
    }
}

4. Interface Segregation Principle (ISP)

This principle states that no client should be forced to depend on methods it does not use.

Before Applying ISP:

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

public class RobotWorker : IWorker
{
    public void Work()
    {
        // Work implementation
    }

    public void Eat()
    {
        // Robot doesn't eat, this will throw an exception
        throw new NotImplementedException("Robots do not eat");
    }
}

After Applying ISP:

public interface IWorker
{
    void Work();
}

public interface IEater
{
    void Eat();
}

public class HumanWorker : IWorker, IEater
{
    public void Work()
    {
        // Human work implementation
    }

    public void Eat()
    {
        // Human eat implementation
    }
}

public class RobotWorker : IWorker
{
    public void Work()
    {
        // Robot work implementation
    }

    // No need to implement the Eat method
}

5. Dependency Inversion Principle (DIP)

This principle states that high-level modules should not depend on low-level modules, but both should depend on abstractions.

Before Applying DIP:

public class EmailService
{
    public void SendEmail(string message)
    {
        // Email sending logic
    }
}

public class OrderService
{
    private EmailService _emailService;

    public OrderService()
    {
        _emailService = new EmailService();
    }

    public void PlaceOrder(string orderDetails)
    {
        // Order processing logic
        _emailService.SendEmail(orderDetails);
    }
}

After Applying DIP:

Top 10 Interview Questions & Answers on SOLID Principles in C#

Top 10 Questions and Answers on SOLID Principles in C#

1. What is the Single Responsibility Principle (SRP) in C# and provide an example?

Answer: The Single Responsibility Principle states that a class should have one and only one reason to change, meaning it should have only one job. For example, in a banking application, there could be a class called WithdrawalService. It should only be responsible for handling withdrawals.

2. How does the Open/Closed Principle (OCP) ensure software maintainability in C#?

Answer: The Open/Closed Principle states that software entities should be open for extension but closed for modification. This means that the behavior of a class can be extended without altering its source code. In C#, this can be achieved through inheritance or interfaces. For example, adding new shapes to a graphics program without modifying the existing drawing code.

public interface IDrawable
{
    void Draw();
}

public class Square : IDrawable
{
    public void Draw() 
    {
        Console.WriteLine("Drawing Square");
    }
}

public class Circle : IDrawable
{
    public void Draw() 
    {
        Console.WriteLine("Drawing Circle");
    }
}

public class Canvas
{
    public void DrawShape(IDrawable drawable)
    {
        drawable.Draw();
    }
}

3. What does the Liskov Substitution Principle (LSP) entail in C#?

Answer: The Liskov Substitution Principle states that objects of a superclass shall be replaceable with objects of its subclasses without affecting the correctness of the program. This means that a subclass should fully obey the behavior of its superclass. Ensuring that a subclass can stand in for its parent class is crucial.

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

public class Pengiun : Bird
{
    public override void Fly()
    {
        throw new Exception("Penguins cannot fly!");
    }
}

In the above example, the LSP is violated as Penguin cannot "fly" like other birds, causing the behavior to be unpredictable.

4. Can you explain the Interface Segregation Principle (ISP) and give an example in C#?

Answer: The Interface Segregation Principle states that no client should be forced to depend on methods it does not use. Instead of having a large interface, multiple smaller interfaces should be created. A class can then implement only the interfaces it requires.

public interface IWorker
{
    void Work();
}

public interface IEater
{
    void Eat();
}

public class HumanWorker : IWorker, IEater
{
    public void Work() { /* ... */ }
    public void Eat() { /* ... */ }
}

public class RobotWorker : IWorker
{
    public void Work() { /* ... */ }
}

5. What is Dependency Inversion Principle (DIP) and how is it applied in C#?

Answer: The Dependency Inversion Principle states that high-level modules should not depend on low-level modules; both should depend on abstractions. In addition, abstractions should not depend on details; details should depend on abstractions. Dependency Injection is a common way to implement this principle in C#.

public interface ILogger
{
    void Log();
}

public class ConsoleLogger : ILogger
{
    public void Log()
    {
        Console.WriteLine("Logging to console.");
    }
}

public class FileLogger : ILogger
{
    public void Log()
    {
        Console.WriteLine("Logging to file.");
    }
}

public class JobProcessor
{
    private readonly ILogger _logger;

    public JobProcessor(ILogger logger)
    {
        _logger = logger;
    }

    public void ProcessJob()
    {
        _logger.Log();
        Console.WriteLine("Processing Job");
    }
}

6. How can I ensure that my class adheres to the Single Responsibility Principle?

Answer: Adhere to SRP by focusing on the single responsibility of a class. If a class handles too many features, break it down into more focused classes. For example, if a Customer class is doing customer validation and customer data access, it's better to separate these concerns into CustomerValidation and CustomerDataAccess classes.

7. Why is the Open/Closed Principle so important in C# software development?

Answer: OCP promotes flexible and reusable design. It allows adding new functionality with minimal intrusive changes to existing code, thus reducing the risk of breaking existing functionality. It makes the codebase easier to evolve and maintain.

8. What role does the Liskov Substitution Principle play in making code more testable?

Answer: LSP ensures that objects can be tested in isolation without unpredictable outcomes. When a subclass can replace its superclass, tests written for the superclass can also be applied to the subclass, ensuring consistency in behavior.

9. Can you use the Interface Segregation Principle (ISP) to avoid the God Object in C#?

Answer: Yes, ISP helps in avoiding the God Object by breaking down a large interface into smaller, more specific ones. This prevents a single class from implementing methods it does not need, adhering to SRP and making the code more modular and maintainable.

10. Why should developers be concerned about the Dependency Inversion Principle when designing C# applications?

Answer: DIP promotes loose coupling and high cohesion, which are essential for building scalable and maintainable applications. By relying on abstractions rather than concrete implementations, developers can create systems that are more adaptable to change and easier to test.

You May Like This Related .NET Topic

Login to post a comment.