Post

ASP.NET | SOLID Principles, and Clean Architecture

Introduction

In modern software development, adhering to principles like SOLID and following Clean Architecture patterns is crucial for building scalable, maintainable, and testable applications. This markdown file aims to provide an overview of these concepts within the context of ASP.NET Core using C# examples.

SOLID Principles

SOLID is an acronym representing five key principles of object-oriented programming and design. They are:

  1. Single Responsibility Principle (SRP): A class should have only one reason to change.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
     public class UserManager
     {
         public void AddUser(User user)
         {
             // Add user to database
         }
    
         public void DeleteUser(User user)
         {
             // Delete user from database
         }
     }
    
  2. Open/Closed Principle (OCP): Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
     public abstract class Shape
     {
         public abstract double Area();
     }
    
     public class Rectangle : Shape
     {
         public double Width { get; set; }
         public double Height { get; set; }
    
         public override double Area()
         {
             return Width * Height;
         }
     }
    
     public class Circle : Shape
     {
         public double Radius { get; set; }
    
         public override double Area()
         {
             return Math.PI * Radius * Radius;
         }
     }
    
  3. Liskov Substitution Principle (LSP): Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
     public class Rectangle
     {
         public virtual int Height { get; set; }
         public virtual int Width { get; set; }
    
         public int Area()
         {
             return Height * Width;
         }
     }
    
     public class Square : Rectangle
     {
         private int _side;
    
         public override int Height
         {
             get => _side;
             set => _side = value;
         }
    
         public override int Width
         {
             get => _side;
             set => _side = value;
         }
     }
    
  4. Interface Segregation Principle (ISP): A client should not be forced to implement an interface that it doesn’t use.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
     public interface IShape
     {
         double Area();
     }
    
     public interface IResizable
     {
         void Resize(double factor);
     }
    
     public class Square : IShape, IResizable
     {
         private double _side;
    
         public double Area()
         {
             return _side * _side;
         }
    
         public void Resize(double factor)
         {
             _side *= factor;
         }
     }
    
  5. 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.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
     public interface ILogger
     {
         void Log(string message);
     }
    
     public class FileLogger : ILogger
     {
         public void Log(string message)
         {
             // Log message to file
         }
     }
    
     public class DatabaseLogger : ILogger
     {
         public void Log(string message)
         {
             // Log message to database
         }
     }
    

Clean Architecture in .NET

Clean Architecture emphasizes separation of concerns and maintaining a clear distinction between business logic, application layers, and external dependencies.

Layers of Clean Architecture

  1. Entities: Contain business objects or business data structures.
  2. Use Cases (Interactors or Application Services): Contain application-specific business rules.
  3. Interfaces (Adapters): Act as bridges between the use cases and external systems.
  4. Frameworks and Drivers: Includes frameworks such as databases, web frameworks, etc.

Example Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
MyApp.Core
├── Entities
│   ├── User.cs
│   └── ...
├── UseCases
│   ├── AddUserUseCase.cs
│   ├── DeleteUserUseCase.cs
│   └── ...
└── Interfaces
    ├── IUserRepository.cs
    └── ...
MyApp.Infrastructure
├── Repositories
│   ├── UserRepository.cs
│   └── ...
└── ExternalServices
    ├── EmailService.cs
    └── ...
MyApp.Presentation
├── Controllers
│   ├── UserController.cs
│   └── ...
└── Views
    ├── Index.cshtml
    └── ...

Example Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// Core layer
namespace MyApp.Core.Entities
{
    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
        // other properties
    }
}

namespace MyApp.Core.UseCases
{
    public class AddUserUseCase
    {
        private readonly IUserRepository _userRepository;

        public AddUserUseCase(IUserRepository userRepository)
        {
            _userRepository = userRepository;
        }

        public void Execute(User user)
        {
            // Validation, business logic, etc.
            _userRepository.Add(user);
        }
    }
}

// Infrastructure layer
namespace MyApp.Infrastructure.Repositories
{
    public class UserRepository : IUserRepository
    {
        public void Add(User user)
        {
            // Add user to the database
        }

        // other repository methods
    }
}

// Presentation layer
namespace MyApp.Presentation.Controllers
{
    public class UserController : Controller
    {
        private readonly AddUserUseCase _addUserUseCase;

        public UserController(AddUserUseCase addUserUseCase)
        {
            _addUserUseCase = addUserUseCase;
        }

        public IActionResult AddUser(User user)
        {
            _addUserUseCase.Execute(user);
            return RedirectToAction("Index");
        }
    }
}

What Next?

By incorporating SOLID principles and Clean Architecture patterns into ASP.NET Core applications, developers can achieve code that is more modular, maintainable, and adaptable to changes in requirements or technologies.

This post is licensed under CC BY 4.0 by the author.