SOLID Principles in C#

Posted on May 7, 2023
.NET Corebeginnerscareer.NET Core 6.0CSharpSOLID PrinciplesBest PracticesSOLID Principles in C#SOLID design patterns in C#C# SOLID design principles

What is SOLID?

The SOLID principle is a set of five design principles that are used to guide the development of software systems that are easy to maintain, extend, and modify. These principles are:

  • S stands for SRP (Single responsibility principle)

  • O stands for OCP (Open closed principle)

  • L stands for LSP (Liskov substitution principle)

  • I stands for ISP ( Interface segregation principle)

  • D stands for DIP ( Dependency inversion principle)

So let’s start understanding each principle with simple c# examples. To illustrate the implementation of solid principles, we'll be utilizing the following draft code throughout this article.

SOLID Principle Image

We will use the User example to understand SOLID Principle.

Use the provided example as a model for creating your own implementation.

Let's understand and try to improve this code by applying the SOLID Principles.

User.cs

namespace codehacks_solid_principle_demo
{
    public class User
    {
        private String name;
        private String email;
        private String password;
        private int age;

        public User(String name, String email, String password, int age)
        {
            this.name = name;
            this.email = email;
            this.password = password;
            this.age = age;
        }

        // Getter and setter methods for all properties

        // Validate the user's email address
        public bool validateEmail()
        {
            // Implementation code
        }

        // Hash the user's password
        public String hashPassword()
        {
            // Implementation code
        }
    }
}

UserRepository.cs

namespace codehacks_solid_principle_demo
{
    public class UserRepository
    {
        public void addUser(User user)
        {
            // Implementation code to add a user to a database
        }

        public User getUserByEmail(String email)
        {
            // Implementation code to get a user from a database by email
        }

        public void updateUser(User user)
        {
            // Implementation code to update a user in a database
        }

        public void deleteUser(User user)
        {
            // Implementation code to delete a user from a database
        }
    }
}

UserController.cs

namespace codehacks_solid_principle_demo
{
    public class UserController
    {
        private UserRepository userRepository;

        public UserController(UserRepository userRepository)
        {
            this.userRepository = userRepository;
        }

        // Create a new user account
        public void registerUser(String name, String email, String password, int age)
        {
            User user = new User(name, email, password, age);
            bool isValidEmail = user.validateEmail();

            if (isValidEmail)
            {
                String hashedPassword = user.hashPassword();
                user.setPassword(hashedPassword);
                userRepository.addUser(user);
            }
            else
            {
                // Handle invalid email address error
            }
        }

        // Authenticate a user login
        public void loginUser(String email, String password)
        {
            User user = userRepository.getUserByEmail(email);
            String hashedPassword = user.hashPassword();

            if (hashedPassword.equals(password))
            {
                // User is authenticated, redirect to dashboard
            }
            else
            {
                // Handle invalid login credentials error
            }
        }

        // Update user profile information
        public void updateUserProfile(User user, String newName, String newEmail, int newAge)
        {
            user.setName(newName);
            user.setEmail(newEmail);
            user.setAge(newAge);
            userRepository.updateUser(user);
        }

        // Delete user account
        public void deleteUserAccount(User user)
        {
            userRepository.deleteUser(user);
        }
    }
}

Understanding “S”- SRP (Single responsibility principle)

Single Responsibility Principle (SRP): A class should have only one reason to change. This means that a class should only have one responsibility or job. If a class has multiple responsibilities, it can become difficult to maintain and modify.

SRP

Problem?

This code violates the Single Responsibility Principle (SRP) because the User class has two responsibilities: representing a user and also validating the user's email and hashing the user's password.

Solution with “S”- SRP

In this updated code, we have separated the responsibilities of the User class into different classes: EmailValidator for email validation and PasswordHasher for password hashing. The UserController class now depends on these new classes instead of having those responsibilities within the User class.

 public class User
    {
        private String name;
        private String email;
        private String password;
        private int age;

        public User(String name, String email, String password, int age)
        {
            this.name = name;
            this.email = email;
            this.password = password;
            this.age = age;
        }

        // Getter and setter methods for all properties

        // Getters for private properties
        public String getName()
        {
            return name;
        }

        public String getEmail()
        {
            return email;
        }

        public String getPassword()
        {
            return password;
        }

        public int getAge()
        {
            return age;
        }
    }
  public class PasswordHasher
    {
        public String hashPassword(String password)
        {
            // Implementation code
        }
    }
   public class EmailValidator
    {
        public boolean validateEmail(String email)
        {
            // Implementation code
        }
    }

Understanding “O” - Open-closed principle

Open/Closed Principle (OCP): Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that you should be able to extend the behavior of a class without changing its source code.

OCP

Let’s continue with our same user class example.

Problem?

The business now requires email and password validation, and they want to ensure minimal effort in the future if they decide to add more validators.

Solution with “O”- OCP

In this updated code, we have implemented the OCP principle by introducing interfaces, UserValidator which are open for extension but closed for modification. Overall, this updated code adheres to the OCP principle by allowing us to add or remove functionality without modifying existing code.

    public interface IUserValidator
    {
        bool Validate(User user);
    }

    public class EmailValidator : IUserValidator
    {
        public bool Validate(User user)
        {
            // Implementation code to validate user email
        }
    }

    public class PasswordValidator : IUserValidator
    {
        public bool Validate(User user)
        {
            // Implementation code to validate user password
        }
    }

Understanding “L”- LSP (Liskov substitution principle)

Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types. This means that any instance of a subclass should be able to be used in place of an instance of its superclass without causing any problems.

LSP

Problem?

Let's say business want to create a new type of user called PremiumUser. This user type will have some additional properties and functionality compared to the regular User.

Solution with “L”- LSP

One way to adhere to the LSP is to create a new class called PremiumUser that inherits from the User class and overrides its methods as necessary. Here is an example:

In this example, the PremiumUser class inherits from the User class and overrides its validateEmail() and hashPassword() methods as necessary. It also has an additional property creditCardNumber and a new method makePayment() that processes payments using the credit card number.

namespace codehacks_solid_principle_demo
{
    public class User
    {
        // ...

        public virtual bool validateEmail()
        {
            // Implementation code
        }

        public virtual String hashPassword()
        {
            // Implementation code
        }
    }

    public class PremiumUser : User
    {
        private String creditCardNumber;

        public PremiumUser(String name, String email, String password, int age, String creditCardNumber)
            : base(name, email, password, age)
        {
            this.creditCardNumber = creditCardNumber;
        }

        public override bool validateEmail()
        {
            // Implementation code with additional validation logic
        }

        public override String hashPassword()
        {
            // Implementation code with different hashing algorithm
        }

        public void makePayment(decimal amount)
        {
            // Implementation code to process payment using credit card number
        }
    }

    // ...
}

Understanding “I” - ISP (Interface Segregation principle)

Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use. This means that you should split up larger interfaces into smaller, more specific interfaces so that clients only need to depend on the relevant interfaces.

Problem?

The UserRepository class has methods that are responsible for both retrieving data from the database and modifying data in the database. This violates the Interface Segregation Principle (ISP) which states that clients should not be forced to depend on interfaces that they do not use.

Solution with “I”- ISP

By segregating the interface into two separate interfaces i.e. IRepositoryProjection and IRepositoryCrudOperation, based on their responsibilities, we can avoid this problem. Clients of the UserRepository class can then choose which interface to implement, based on their specific needs. This leads to a more flexible and maintainable codebase, and also allows for easier testing of the code.

namespace codehacks_solid_principle_demo
{
    public interface IRepositoryProjection
    {
        User GetUserByEmail(string email);
        List<User> GetAll();
    }
}
namespace codehacks_solid_principle_demo
{
    public interface IRepositoryCrudOperation
    {
        void AddUser(User user);
        void UpdateUser(User user);
        void DeleteUser(User user);
    }
}
namespace codehacks_solid_principle_demo
{
    public class UserRepository : IRepositoryProjection, IRepositoryCrudOperation
    {
        public void AddUser(User user)
        {
            // Implementation code to add a user to a database
        }

        public User GetUserByEmail(string email)
        {
            // Implementation code to get a user from a database by email
        }

        public List<User> GetAll()
        {
            // Implementation code get all users from a database
        }

        public void UpdateUser(User user)
        {
            // Implementation code to update a user in a database
        }

        public void DeleteUser(User user)
        {
            // Implementation code to delete a user from a database
        }
    }
}

Understanding “D”- Dependency inversion principle

Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions. This means that you should depend on abstractions, not on concrete implementations.

DIP

Problem?

the UserRepository class is tightly coupled to the User class. This means that any changes made to the User class could potentially require changes to the UserRepository class. In the absence of DIP, making changes to the User class could also require changes to the UserRepository class, which could lead to a cascade of changes throughout the codebase. This makes the code harder to maintain and increases the risk of introducing bugs or other errors during development.

Solution with “D”- DIP

Dependency Inversion Principle (DIP), we need to ensure that the UserRepository class depends on abstractions, rather than concrete implementations. This helps to decouple the class from its dependencies, and makes it easier to replace those dependencies in the future without affecting the UserRepository class.

namespace codehacks_solid_principle_demo
{
    public interface IUserRepository
    {
        void AddUser(User user);
        User GetUserByEmail(string email);
        void UpdateUser(User user);
        void DeleteUser(User user);
    }
}
namespace codehacks_solid_principle_demo
{
    public class UserRepository : IUserRepository
    {
        public void AddUser(User user)
        {
            // Implementation code to add a user to a database
        }

        public User GetUserByEmail(string email)
        {
            // Implementation code to get a user from a database by email
        }

        public void UpdateUser(User user)
        {
            // Implementation code to update a user in a database
        }

        public void DeleteUser(User user)
        {
            // Implementation code to delete a user from a database
        }
    }
}

For example, suppose we have a UserService class that depends on a IUserRepository instance:

namespace codehacks_solid_principle_demo
{
    public class UserService
    {
        private readonly IUserRepository userRepository;

        public UserService(IUserRepository userRepository)
        {
            this.userRepository = userRepository;
        }

        public void CreateUser(User user)
        {
            // Implementation code to create a new user
        }

        public void UpdateUser(User user)
        {
            // Implementation code to update an existing user
        }

        public void DeleteUser(User user)
        {
            // Implementation code to delete a user
        }
    }
}

Summary

SOLID is a set of principles in object-oriented programming that aim to make software systems more maintainable, extensible, and easier to understand.

SOLID stands for:

  • Single Responsibility Principle (SRP): A class should have only one reason to change.
  • Open/Closed Principle (OCP): A class should be open for extension but closed for modification.
  • Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types.
  • Interface Segregation Principle (ISP): A client should not be forced to depend on methods it does not use.
  • Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions.

These principles help to reduce code complexity, improve code maintainability, and increase the overall quality of software systems. By following SOLID, developers can build systems that are more modular, easier to test, and more resilient to change.

Thanks for reading!


Posted on May 7, 2023

Anonymous User

November 2, 2023

thanks it is very helpful

Anonymous User

May 26, 2023

Profile Picture

Arun Yadav

Software Architect | Full Stack Web Developer | Cloud/Containers

Subscribe
to our Newsletter

Signup for our weekly newsletter to get the latest news, articles and update in your inbox.

More Related Articles