SOLID Principles and Design Pattern.

What is the SOLID Principle?

SOLID is an acronym for five key principles of object-oriented design that help create clean, scalable, testable, and maintainable code. These principles are especially useful when building layered applications like ASP.NET Core Web APIs or MVC apps.

SOLID stands for:

  • S - Single Responsibility Principle
  • O - Open/Closed Principle
  • L - Liskov Substitution Principle
  • I - Interface Segregation Principle
  • D - Dependency Inversion Principle

Let's understand each point with an example:

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change.

In ASP.NET Core, separate business logic, data access, and controller logic.

❌ Bad Example (Violates SRP).

public class ProductService
{
    public void Save(Product p) { /* save to DB */ }
    public void SendEmail(Product p) { /* email logic */ }
}

✅ Good Example.
public class ProductService
{
    private readonly IProductRepository _repo;
    public ProductService(IProductRepository repo) => _repo = repo;
    
    public void Save(Product product) => _repo.Add(product);
}

public class EmailService
{
    public void SendEmail(Product p) { /* only email logic */ }
}

2. Open/Closed Principle (OCP)
Definition: Classes should be open for extension but closed for modification.
In ASP.NET Core, use interfaces and inheritance to extend behavior without changing existing code.

Example: Logging Different Formats
public interface ILoggerService
{
    void Log(string message);
}

public class FileLogger : ILoggerService
{
    public void Log(string message) { /* log to file */ }
}

public class DbLogger : ILoggerService
{
    public void Log(string message) { /* log to DB */ }
}

The system can now be extended with a new logger without modifying the existing services.

3. Liskov Substitution Principle (LSP)
Definition: Subclasses should be replaceable for their base class without breaking the app.
In ASP.NET Core, ensure service implementations follow expected behavior.

❌ Violates LSP
If one subclass throws NotImplementedException:
public class BrokenNotificationService : NotificationService
{
    public override void Send(string message)
    {
        throw new NotImplementedException(); // ❌ Violates LSP
    }
}

Good Example:
We have a base class, NotificationService, and two derived classes: EmailNotificationService and SmsNotificationService.

Base Class:
public abstract class NotificationService
{
    public abstract void Send(string message);
}

Derived Class:
public class EmailNotificationService : NotificationService
{
    public override void Send(string message)
    {
        Console.WriteLine($"Email sent: {message}");
    }
}

public class SmsNotificationService : NotificationService
{
    public override void Send(string message)
    {
        Console.WriteLine($"SMS sent: {message}");
    }
}

Usage (LSP in Action)
public class NotificationController
{
    private readonly NotificationService _service;

    public NotificationController(NotificationService service)
    {
        _service = service;
    }

    public void NotifyUser()
    {
        _service.Send("Hello, User!");
    }
}

You can now substitute EmailNotificationService or SmsNotificationService when injecting into NotificationController without changing any controller code:
var controller1 = new NotificationController(new EmailNotificationService());
controller1.NotifyUser(); // Output: Email sent: Hello, User!

var controller2 = new NotificationController(new SmsNotificationService());
controller2.NotifyUser(); // Output: SMS sent: Hello, User!
  • Inheritance + LSP = Child classes must work in place of their base class.
  • Avoid incomplete or broken implementations in subclasses.
  • In ASP.NET Core, always ensure services or controller dependencies fully implement expected behavior.

4. Interface Segregation Principle (ISP)
Definition: Don’t force classes to implement unnecessary methods.
In ASP.NET Core, use smaller, specific interfaces.

❌ Bad Interface
public interface IProductService
{
    void Add();
    void Update();
    void ExportToExcel(); // not every implementation needs this
}

✅ Good Design
public interface ICrudService
{
    void Add();
    void Update();
}

public interface IExportService
{
    void ExportToExcel();
}

5. Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules. Instead, both should depend on abstractions.
In ASP.NET Core, this is implemented via dependency injection (DI).
Example:
public interface IEmailService
{
    void Send(string to, string message);
}

public class SmtpEmailService : IEmailService
{
    public void Send(string to, string message)
    {
        // logic to send email
    }
}

public class NotificationController : ControllerBase
{
    private readonly IEmailService _emailService;

    public NotificationController(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public IActionResult Notify()
    {
        _emailService.Send("user@example.com", "Hello!");
        return Ok();
    }
}

Applying SOLID principles in ASP.NET Core helps you build robust, clean, and future-proof applications. These principles make your code:
  • Easier to test
  • Simpler to maintain
  • More scalable and flexible
I hope you understood the SOLID Principle and use case now. Let's understand a few popular Design Patterns that we follow while writing ASP.NET Core Code.

Design Patterns

1. Singleton Pattern.

The Singleton Pattern ensures that a class has only one instance throughout the application’s lifetime and provides a global point of access to that instance.

When to Use Singleton in ASP.NET Core
  • Logging services
  • Configuration settings
  • Caching
  • Shared services (only if thread-safe and stateless)
Example: Think of Logger: You want one shared logger throughout your app rather than creating a new one every time.

Step 1: Create the Singleton Class
public class AppLogger
{
    // Static instance
    private static readonly AppLogger _instance = new AppLogger();

    // Private constructor
    private AppLogger() { }

    // Public static property to access the instance
    public static AppLogger Instance => _instance;

    // Sample method
    public void Log(string message)
    {
        Console.WriteLine($"[LOG] {DateTime.Now}: {message}");
    }
}

Usage in Code:
class Program
{
    static void Main(string[] args)
    {
        var logger1 = AppLogger.Instance;
        var logger2 = AppLogger.Instance;

        logger1.Log("Singleton is working!");
        logger2.Log("Same instance is used again.");

        Console.WriteLine(ReferenceEquals(logger1, logger2)); // Output: True
    }
}

2. Factory Pattern.

The Factory Pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created.
In simpler terms: You let a Factory class decide which object to create based on input, instead of using new everywhere.

In short, "The Factory Pattern lets me abstract the creation of objects based on input or logic. Instead of using new everywhere, I call a Factory method, which decides what class to return. This keeps my code flexible and clean."

When to Use the Factory Pattern
  • When object creation logic is complex or repetitive
  • When you want to decouple object creation from your main logic
  • When the class to instantiate is determined at runtime

Example: You want to send different types of notifications — Email, SMS, or Push.

Step 1: Create a Common Interface.
public interface INotification
{
    void Send(string message);
}

Step 2: Create Concrete Implementations.
public class EmailNotification : INotification
{
    public void Send(string message)
    {
        Console.WriteLine("Email sent: " + message);
    }
}

public class SmsNotification : INotification
{
    public void Send(string message)
    {
        Console.WriteLine("SMS sent: " + message);
    }
}

public class PushNotification : INotification
{
    public void Send(string message)
    {
        Console.WriteLine("Push sent: " + message);
    }
}

Step 3: Create the Factory Class.
public class NotificationFactory
{
    public static INotification Create(string type)
    {
        return type.ToLower() switch
        {
            "email" => new EmailNotification(),
            "sms" => new SmsNotification(),
            "push" => new PushNotification(),
            _ => throw new ArgumentException("Invalid notification type"),
        };
    }
}

Step 4: Use the Factory in Your App.
class Program
{
    static void Main()
    {
        Console.Write("Enter notification type (email/sms/push): ");
        string type = Console.ReadLine();

        INotification notification = NotificationFactory.Create(type);
        notification.Send("Hello from Factory Pattern!");
    }
}

3. Repository Pattern.

The Repository Pattern is used to abstract the data access layer so your application code (like services or controllers) doesn't directly interact with the database.
This pattern helps:
  • Centralized database logic
  • Make the code more testable
  • Enforce Separation of Concerns (SoC)

Example:
Think of the repository as a middleman between your app and the database.
Instead of writing EF Core queries inside your controller, you use a repository.

Step 1: Define a Model.
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Step 2: Create the DbContext.
public class AppDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }

    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
    }
}

Step 3: Create Repository Interface.
public interface IProductRepository
{
    IEnumerable<Product> GetAll();
    Product GetById(int id);
    void Add(Product product);
    void Update(Product product);
    void Delete(int id);
}

Step 4: Implement Repository.
public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _context;
    public ProductRepository(AppDbContext context)
    {
        _context = context;
    }

    public IEnumerable<Product> GetAll() => _context.Products.ToList();

    public Product GetById(int id) => _context.Products.Find(id);

    public void Add(Product product)
    {
        _context.Products.Add(product);
        _context.SaveChanges();
    }

    public void Update(Product product)
    {
        _context.Products.Update(product);
        _context.SaveChanges();
    }

    public void Delete(int id)
    {
        var product = _context.Products.Find(id);
        if (product != null)
        {
            _context.Products.Remove(product);
            _context.SaveChanges();
        }
    }
}

Step 5: Register in DI (Program.cs).
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

Step 6: Use the Repository in the Controller.
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductRepository _repo;

    public ProductsController(IProductRepository repo)
    {
        _repo = repo;
    }

    [HttpGet]
    public IActionResult Get() => Ok(_repo.GetAll());

    [HttpPost]
    public IActionResult Post(Product product)
    {
        _repo.Add(product);
        return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
    }
}

Q: Why use the repository pattern?
Answer: "It abstracts the data access logic, helps me write unit tests easily, and makes the app follow SOLID principles, especially SRP and DIP."

⚡ Please share your valuable feedback and suggestion in the comment section below or you can send us an email on our offical email id ✉ algolesson@gmail.com. You can also support our work by buying a cup of coffee ☕ for us.

Similar Posts

No comments:

Post a Comment


CLOSE ADS
CLOSE ADS