Ads

C# Programming 26 - Building Robust ASP.NET Core Web API Using SOLID Principles


Hello All,

I am Chakrapani Upadhyaya, In this article we will learn about SOLID Principles in .Net Core We API.

In the ever-evolving landscape of software development, creating maintainable and scalable applications is crucial. The SOLID principles offer a set of guidelines that promote clean, modular, and extensible code. In this article, we will explore how to implement SOLID principles in the context of an ASP.NET Core Web API, ensuring a solid foundation for your application's growth and maintainability.




Understanding SOLID Principles:

SOLID is an acronym that represents a set of five design principles, each focusing on a specific aspect of building robust and maintainable software:

  • Single Responsibility Principle (SRP): 
A class should have only one reason to change. It should have only one responsibility or job.

  • Open/Closed Principle (OCP): 
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

  • Liskov Substitution Principle (LSP): 
Subtypes must be substitutable for their base types without altering the correctness of the program.

  • Interface Segregation Principle (ISP): 
A class should not be forced to implement interfaces it does not use.

  • Dependency Inversion Principle (DIP): 
High-level modules should not depend on low-level modules. Both should depend on abstractions, and abstractions should not depend on details.

S - Applying SRP to ASP.NET Core Web API:


Consider a scenario where you have a UserController that handles user-related operations. Following the SRP, it's advisable to split responsibilities. You can create a separate UserService responsible for user-related business logic, promoting a clean separation of concerns.


public class UserController : ControllerBase
{
    private readonly IUserService _userService;

    public UserController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpGet("{id}")]
    public IActionResult GetUserById(int id)
    {
        var user = _userService.GetUserById(id);
        return Ok(user);
    }

    // Other user-related actions...
}


O - Implementing OCP with Dependency Injection:


Applying the Open/Closed Principle involves designing the system in a way that allows you to add new features without modifying existing code. Utilize dependency injection in ASP.NET Core to inject dependencies into your controllers, making them open for extension.

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IUserService, UserService>();
    // Add other services...
}


L - Ensuring LSP in Model Inheritance:


When dealing with model inheritance, make sure that derived types can be substituted for their base types without affecting the correctness of the application. This is crucial for maintaining a consistent and predictable behavior.


public class User
{
    public int Id { get; set; }
    public string UserName { get; set; }
}

public class AdminUser : User
{
    public bool IsAdmin { get; set; }
}


Imagine you have a base class called "User," which has some basic information like "Id" and "UserName." Then, you create another class called "AdminUser" that adds a little extra information, like whether the user is an admin with the property "IsAdmin."

Now, the idea is that you can use an "AdminUser" wherever you would use a "User" without causing any problems in your program. Because "AdminUser" is derived from "User," it inherits all the properties of "User" and adds its own.

In simple terms, if you have a function that works with "User" objects, you should be able to give it an "AdminUser" object, and everything should still work correctly. This ability to seamlessly switch between the base type ("User") and its derived type ("AdminUser") without breaking things is what makes your code consistent and predictable.


I - Adhering to ISP with Segregated Interfaces:

To comply with the Interface Segregation Principle, create small, focused interfaces tailored to the needs of specific classes. Avoid creating monolithic interfaces that force implementing classes to provide unnecessary functionality.


public interface IUserService
{
    User GetUserById(int id);
    // Other user-related methods...
}

public interface IAdminService
{
    bool PromoteToAdmin(User user);
    // Other admin-related methods...
}


Let's break down the Interface Segregation Principle in simpler terms using your code:

When you're creating interfaces (a set of rules that classes must follow), it's a good idea to keep them small and specific. In your example:

IUserService is for services related to regular users. It has a method to get a user by ID and potentially other methods specific to regular users.

IAdminService is for services related to admin users. It has a method to promote a user to admin and maybe other methods for admin-specific tasks.

This follows the Interface Segregation Principle, which says that you should avoid making one big interface that forces a class to implement methods it doesn't need. Instead, create several small interfaces, each with a specific purpose.

Imagine you have a class that only deals with regular users, not admins. If you had a big, all-encompassing interface, that class would be forced to implement admin-related methods, even though it doesn't use them. This could lead to confusion and unnecessary work.

By having small, focused interfaces, classes can choose to implement only what they need. It keeps things clear, avoids unnecessary work, and makes your code more flexible.

In simpler terms, it's like giving each class a job description with only the tasks they actually need to do, rather than making everyone follow a universal job description that might include tasks irrelevant to them.

D - Dependency Inversion with Abstractions:


Ensure Dependency Inversion by depending on abstractions rather than concrete implementations. This facilitates flexibility and makes it easier to switch implementations when needed.

public class UserController : ControllerBase
{
    private readonly IUserService _userService;

    public UserController(IUserService userService)
    {
        _userService = userService;
    }

    // Controller actions...
}


Dependency Inversion is like saying, "Don't depend on specific things; depend on more general things." In your example:

UserController needs something to work with related to users, and it gets that something through the IUserService interface.

Instead of saying, "I absolutely need a specific UserService," it says, "I just need something that follows the rules laid out in IUserService."

Why is this a good thing?

Flexibility: If tomorrow you create a new kind of user service, say, SuperUserService, as long as it follows the rules in IUserService, you can easily switch it in. The UserController doesn't care about the details; it just wants something that behaves like a user service.

Easier Changes: Let's say you want to test UserController independently. You can create a mock user service that follows IUserService, and you're good to go. You're not stuck with the real user service; you can use a stand-in for testing.

In simpler words, the UserController isn't picky about the exact user service it gets. It just wants something that fits the job description (the rules in IUserService). This makes your code more adaptable and easier to change without causing a fuss.

Finally:

By incorporating SOLID principles into your ASP.NET Core Web API development, you establish a robust and maintainable foundation for your application. This not only enhances code readability and scalability but also makes future modifications and feature additions more straightforward. Following these principles ensures a flexible architecture that can adapt to the evolving requirements of your project.


Post a Comment

0 Comments
* Please Don't Spam Here. All the Comments are Reviewed by Admin.

#buttons=(Accept !) #days=(20)

Our website uses cookies to enhance your experience. Learn More
Accept !