How To Resolve Circular Dependency Error in Angular.

In Angular, sometimes your code gets stuck, like two friends asking each other the same question but never answering. This problem is called a Circular Dependency. It happens when two (or more) parts of your code depend on each other in a loop, and Angular doesn’t know where to start.

Before understanding the process of resolving Circular Dependency, let's first understand what Dependency is and why it is so important in building any software application.

What is Dependency?

In programming, a dependency is a relationship where one piece of code relies on another to function correctly. This is a fundamental concept in software development, as modern applications are rarely built from scratch. Instead, developers reuse existing, pre-written code in the form of libraries, frameworks, or modules.

Dependency Example:

Think of baking a cake. To make the cake (your code), you need flour, sugar, and eggs (dependencies). You don't have to create the flour or sugar yourself; you buy them from a store (like downloading from a library). Without these ingredients, you can't bake the cake. Your cake recipe depends on the ingredients.

In software, dependencies work the same way:
  • A component (like a button on a web page) might depend on a styling library to make it look good.
  • A service that handles user login might depend on a database library to interact with the database.

This dependency relationship is usually one-way. The button needs the styling library, but the styling library doesn't need to know anything about the button. The login service needs the database library, but the database library doesn't need to know anything about the login service.

When you have a dependency, if the code you're depending on changes or is unavailable, your code will likely fail. This is why managing dependencies is a crucial part of software development.

Understanding this one-way relationship is the key to grasping why a circular dependency, where two components depend on each other, is so problematic.

What is Circular Dependency?

A circular dependency occurs when two or more components, modules, or services depend on each other, creating a closed loop. This prevents them from being properly initialized or compiled because each one is waiting for the other to be ready first.

Imagine two people, Alice and Bob.
  • Alice says, "I can't start my project until Bob gives me his budget report."
  • Bob says, "I can't finish my budget report until Alice starts her project, so I can get the final numbers."

They are stuck in an eternal loop. Alice needs Bob's input to begin, and Bob needs Alice's progress to finish. Neither can move forward, and the project is deadlocked. This is a circular dependency.

In a software application, the same principle applies. For example, in a codebase:
  • UserService needs to call a method in PaymentService to process a transaction.
  • PaymentService needs to access a user's details, which it gets by calling a method in UserService.

This creates a tight coupling where UserService depends on PaymentService, and PaymentService depends on UserService. The system can't decide which service to initialize first, leading to errors. This anti-pattern indicates a poor design that makes the code difficult to maintain, test, and reuse.

Let's recreate circular dependency to understand the exact condition and how to resolve it.

Example of Circular Dependency in Angular.

user.service.ts
// user.service.ts
import { Injectable } from '@angular/core';
import { PaymentService } from './payment.service';

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private paymentService: PaymentService) {}

  getUserDetails(userId: number) {
    console.log("Fetching user details for", userId);

    // Oh no ❌ UserService is calling PaymentService
    this.paymentService.getPaymentsForUser(userId);
  }
}

payment.service.ts
// payment.service.ts
import { Injectable } from '@angular/core';
import { UserService } from './user.service';

@Injectable({ providedIn: 'root' })
export class PaymentService {
  constructor(private userService: UserService) {}

  getPaymentsForUser(userId: number) {
    console.log("Fetching payments for", userId);

    // Oh no ❌ PaymentService is calling UserService again
    this.userService.getUserDetails(userId);
  }
}

This Angular code is trying to create a userService, but userService needs PaymentService. Then Angular tries to create PaymentService, but PaymentService needs userService. Angular goes back to create userService...and this cycle never ends. This is a circular dependency.

userService -> PaymentService -> userService
Error: Circular dependency in DI detected for UserService

We need to break the direct cycle by introducing a shared service or event mediator. Let's learn both methods one by one.

Method 1: Resolve Circular Dependency Using Shared Service.

This is the most popular and easiest way to break circular dependency by moving the common logic into a third service that both can use.

Here are a few simple steps that you need to follow:
1. Identify the code that causes a circular reference. 
2. Create a new independent service. 
3. Inject this service into both classes instead of injecting each other.

Fix Example:
This is a mediator service that holds the shared logic.
// data.service.ts
@Injectable({ providedIn: 'root' })
export class DataService {
  getUserDetails(userId: string) {
    console.log("Fetching user details:", userId);
  }

  getPaymentsForUser(userId: string) {
    console.log("Fetching payments for user:", userId);
  }
}

This is the modified userService after introducing the shared service.
// user.service.ts
@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private data: DataService) {}

  getUserDetails(userId: string) {
    this.data.getUserDetails(userId);
  }
}

This is the modified PaymentService after introducing the shared service.
// payment.service.ts
@Injectable({ providedIn: 'root' })
export class PaymentService {
  constructor(private data: DataService) {}

  getPaymentsForUser(userId: string) {
    this.data.getPaymentsForUser(userId);
  }
}

Now both services depend only on DataService → no cycle. This is cleaner and easier to maintain.

Method 2: Use forwardRef(). 

If the dependency is truly unavoidable (say, PaymentService must call UserService directly), we can use forwardRef().

How does forwardRef() Work?

In Angular, when you create a service or class, Angular tries to resolve all dependencies immediately at runtime.
But in circular dependency cases, one class might not be defined yet when Angular tries to use it.

👉 forwardRef() is a helper function that tells Angular:

“Don’t try to resolve this dependency right now — wait until everything is defined, and then resolve it.”

Fix Example:

// user.service.ts
@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(
    @Inject(forwardRef(() => PaymentService)) private payment: PaymentService
  ) {}

  getUserDetails(userId: string) {
    console.log("Fetching user details:", userId);
    this.payment.getPaymentsForUser(userId);
  }
}

// payment.service.ts
@Injectable({ providedIn: 'root' })
export class PaymentService {
  constructor(
    @Inject(forwardRef(() => UserService)) private user: UserService
  ) {}

  getPaymentsForUser(userId: string) {
    console.log("Fetching payments for user:", userId);
    this.user.getUserDetails(userId);
  }
}

Now Angular delays resolution until both services are defined.
The circular error is avoided, but if you keep calling each other directly, there’s still a risk of infinite recursion, so use it carefully.

ASP.NET Core MVC Application for CURD Operation.

 


View Part

1. Create.cshtml

@model BookStorage.Models.Book
@{
    ViewData["Title"] = "Add Book";
}
<h2>Add Book</h2>

<form asp-action="Create" method="post">
    <div class="form-group">
        <label asp-for="Title"></label>
        <input asp-for="Title" class="form-control" />
        <span asp-validation-for="Title" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Author"></label>
        <input asp-for="Author" class="form-control" />
        <span asp-validation-for="Author" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="YearPublished"></label>
        <input asp-for="YearPublished" class="form-control" />
        <span asp-validation-for="YearPublished" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Price"></label>
        <input asp-for="Price" class="form-control" />
        <span asp-validation-for="Price" class="text-danger"></span>
    </div>
    <button type="submit" class="btn btn-success">Save</button>
</form>

2. Edit.cshtml

@model BookStorage.Models.Book
@{
    ViewData["Title"] = "Edit Book";
}
<h2>Edit Book</h2>
<form asp-action="Edit" method="post">
    <input type="hidden" asp-for="BookId" />

    <div class="form-group">
        <label asp-for="Title"></label>
        <input asp-for="Title" class="form-control" />
        <span asp-validation-for="Title" class="text-danger"></span>
    </div>

    <div class="form-group">
        <label asp-for="Author"></label>
        <input asp-for="Author" class="form-control" />
        <span asp-validation-for="Author" class="text-danger"></span>
    </div>

    <div class="form-group">
        <label asp-for="YearPublished"></label>
        <input asp-for="YearPublished" class="form-control" />
        <span asp-validation-for="YearPublished" class="text-danger"></span>
    </div>

    <div class="form-group">
        <label asp-for="Price"></label>
        <input asp-for="Price" class="form-control" />
        <span asp-validation-for="Price" class="text-danger"></span>
    </div>

    <button type="submit" class="btn btn-primary">Update</button>
    <a asp-action="Index" class="btn btn-secondary">Cancel</a>
</form>
@section Scripts {
    @{
        await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }
}

3. Index.cshtml

@model IEnumerable<BookStorage.Models.Book>
@{
    ViewData["Title"] = "Book List";
}
@if(TempData["SuccessMessage"] != null)
{
    <div class="alert alert-success alert-dismissible fade show" role="alert">
        @TempData["SuccessMessage"]
        <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
    </div>
}
<h1>@ViewData["Title"]</h1>
<p>
    <a asp-action="Create" class="btn btn-primary">Add New Book</a>
</p>
<table class="table table-bordered">
    <thead>
        <tr>
            <th>Title</th>
            <th>Author</th>
            <th>Published Year</th>
            <th>Price</th>
            <th></th>
        </tr>
    </thead>
     <tbody>
          @foreach (Book book in Model)
          {
              <tr>
                  <td>@book.Title</td>
                  <td>@book.Author</td>
                  <td>@book.YearPublished</td>
                  <td>@book.Price</td>
                  <td>
                      <a asp-action="Edit" asp-route-id="@book.BookId" class="btn btn-warning">Edit</a> |
                      <a asp-action="Details" asp-route-id="@book.BookId" class="btn btn-info">Details</a> |
                      <a asp-action="Delete" asp-route-id="@book.BookId" class="btn btn-danger">Delete</a>
                  </td>
              </tr>
          }
     </tbody>
</table>

Controller

1. BookController.cs

using BookStorage.Data;
using BookStorage.Models;
using Microsoft.AspNetCore.Mvc;

namespace BookStorage.Controllers
{
    public class BookController : Controller
    {
        private readonly AppDbContext _context;

        public BookController(AppDbContext context)
        {
            _context = context;
        }
        [HttpGet]
        [Route("books")]
        public IActionResult Index()
        {
            return View(_context.Books.ToList());
        }

        [HttpGet]
        public IActionResult Create()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Create(Book book)
        {
            if (ModelState.IsValid)
            {
                book.BookId = Guid.NewGuid();
                _context.Books.Add(book);
                _context.SaveChanges();
                TempData["SuccessMessage"] = "Book created successfully!";
                return RedirectToAction(nameof(Index));
            }
            return View(book);
        }
        [HttpGet]
        public IActionResult Edit(Guid id)
        {
            var book = _context.Books.Find(id);
            if (book == null) return NotFound();
            return View(book);
        }
        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Edit(Guid id, Book book)
        {
            if (id != book.BookId) return NotFound();

            if (ModelState.IsValid)
            {
                _context.Update(book);
                _context.SaveChanges();
                return RedirectToAction(nameof(Index));
            }
            return View(book);
        }
        public IActionResult Delete(Guid id)
        {
            var book = _context.Books.Find(id);
            if (book == null) return NotFound();
            return View(book);
        }
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public IActionResult DeleteConfirmed(Guid id)
        {
            var book = _context.Books.Find(id);
            if (book != null)
            {
                _context.Books.Remove(book);
                _context.SaveChanges();
            }
            return RedirectToAction(nameof(Index));
        }
        public IActionResult Details(Guid id)
        {
            var book = _context.Books.Find(id);
            if (book == null) return NotFound();
            return View(book);
        }
    }
}

Program.cs

using BookStorage.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'BookStorageContext' not found.")));
var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

AppDbContext.cs

using BookStorage.Models;
using Microsoft.EntityFrameworkCore;

namespace BookStorage.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

        public DbSet<Book> Books { get; set; }
    }
}

Types of Data Binding in Angular.

Data binding in Angular is a powerful feature that allows you to synchronize data between the component and the view. It is used to ensure that the user interface reflects the current state of the data model and vice versa. This synchronization helps in creating dynamic and interactive applications.

There are four main types of data binding in Angular:
  • Interpolation
  • Property Binding
  • Event Binding
  • Two-Way Binding

1. Interpolation

Why it's used: Interpolation allows you to display component data in the template. It is a simple way to bind data from the component to the view.

Example:
import { Component } from '@angular/core';

@Component({
  selector: 'app-interpolation',
  template: `<h1>{{ title }}</h1>`
})
export class InterpolationComponent {
  title: string = 'Hello, Angular!';
}

Use Case: Use interpolation when you want to display a string or a number from the component in the template.

2. Property Binding

Why it's used: Property binding allows you to bind component properties to the properties of HTML elements. This is useful for dynamically setting attributes or properties based on the component's state.

Example:
import { Component } from '@angular/core';

@Component({
  selector: 'app-property-binding',
  template: `<img [src]="imageUrl" alt="Image">`
})
export class PropertyBindingComponent {
  imageUrl: string = 'https://example.com/image.png';
}

Use Case: Use property binding when you need to set properties of HTML elements, such as src, disabled, or value, based on the component's data.

3. Event Binding

Why it's used: Event binding allows you to listen to events emitted by DOM elements and execute methods in the component in response. This is essential for handling user interactions.

Example:
import { Component } from '@angular/core';

@Component({
  selector: 'app-event-binding',
  template: `<button (click)="onClick()">Click Me!</button>`
})
export class EventBindingComponent {
  onClick() {
    alert('Button clicked!');
  }
}

Use Case: Use event binding when you want to respond to user actions, such as clicks, key presses, or mouse movements.

4. Two-Way Binding

Why it's used: Two-way binding allows for a two-way synchronization between the component and the view. It is particularly useful in forms where user input needs to be reflected in the component's properties and vice versa.

Example:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-two-way-binding',
  template: `<input [(ngModel)]="name" placeholder="Enter your name">
             <p>Hello, {{ name }}!</p>`
})
export class TwoWayBindingComponent {
  name: string = '';
}

Use Case: Use two-way binding when you need to bind form inputs to component properties, allowing for real-time updates as the user types.

Angular Data Binding Example.

Below is a complete working example of an Angular application that demonstrates all four types of data binding: interpolation, property binding, event binding, and two-way binding.

Step 1: Set Up Angular Application
First, make sure you have Angular CLI installed. You can create a new Angular application using the following command:
ng new data-binding-example
cd data-binding-example

Step 2: Create a Component
Next, create a new component called data-binding:
ng generate component data-binding

Step 3: Update the Component
Now, open the data-binding.component.ts file and update it as follows:
import { Component } from '@angular/core';

@Component({
  selector: 'app-data-binding',
  templateUrl: './data-binding.component.html',
  styleUrls: ['./data-binding.component.css']
})
export class DataBindingComponent {
  title: string = 'Data Binding Example';
  imageUrl: string = 'https://via.placeholder.com/150';
  name: string = '';

  onClick() {
    alert(`Hello, ${this.name}!`);
  }
}

Step 4: Update the Template
Next, open the data-binding.component.html file and update it with the following code:
<h1>{{ title }}</h1> <!-- Interpolation -->

<img [src]="imageUrl" alt="Placeholder Image"> <!-- Property Binding -->

<button (click)="onClick()">Click Me!</button> <!-- Event Binding -->

<input [(ngModel)]="name" placeholder="Enter your name"> <!-- Two-Way Binding -->
<p>Hello, {{ name }}!</p> <!-- Interpolation -->

Step 5: Import FormsModule
To use two-way binding with ngModel, you need to import FormsModule. Open the app.module.ts file and update it as follows:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms'; // Import FormsModule
import { AppComponent } from './app.component';
import { DataBindingComponent } from './data-binding/data-binding.component';

@NgModule({
  declarations: [
    AppComponent,
    DataBindingComponent
  ],
  imports: [
    BrowserModule,
    FormsModule // Add FormsModule to imports
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Step 6: Update the Main Template
Finally, open the app.component.html file and include the data-binding component:
<app-data-binding></app-data-binding>

Step 7: Run the Application
Now, you can run the application using the following command: ng serve

Open your browser and navigate to http://localhost:4200. You should see the following:
  • A title displayed using interpolation.
  • An image is displayed using property binding.
  • A button that shows an alert with a greeting when clicked (event binding).
  • An input field that updates the greeting in real-time using two-way binding.


Summary

  • Interpolation: Displays component data in the template.
  • Property Binding: Binds component properties to HTML element properties.
  • Event Binding: Listens to events and executes component methods.
  • Two-Way Binding: Synchronizes data between the component and the view, especially in forms.
These data binding techniques enable developers to create dynamic and responsive applications in Angular, enhancing user experience and interaction.

Difference Between ngOnInit and constructor in Angular.

In Angular, both the constructor and the ngOnInit lifecycle hook are used in components, but they serve different purposes and are called at different times in the component's lifecycle. Understanding the use and their difference will help you build a better Angular Application.

Constructor

The constructor is a TypeScript feature used to initialize class members. It is called when the component is instantiated. The constructor is called before Angular sets any input properties or runs any lifecycle hooks.

Usage: You typically use the constructor for dependency injection and to initialize class properties. However, you should avoid complex logic or operations that depend on the component's view or input properties.

ngOnInit

ngOnInit is a lifecycle hook provided by Angular that is called after the component's constructor and after Angular has initialized all data-bound properties. It is called once the component is fully initialized, and it is a good place to perform any additional initialization tasks that require access to input properties or the component's view.

Usage: You can use ngOnInit to fetch data from services, set up subscriptions, or perform any other initialization that requires the component to be fully set up.

Difference Between Constructor and ngOnInit.

Feature constructor() ngOnInit()
Purpose Initializes class members and injects dependencies Used for component initialization logic after inputs are set
Lifecycle Runs when the class is instantiated (before Angular binds inputs) Runs after the constructor, when Angular has fully initialized the component
Best Use Dependency injection, initializing simple properties Fetching data, calling APIs, and logic that depends on @Input()
Called By JavaScript engine Angular framework

Example Angular Code:
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-example',
  templateUrl: './example.component.html',
})
export class ExampleComponent implements OnInit {
  data: string;

  constructor() {
    // Constructor logic (e.g., dependency injection)
    console.log('Constructor called');
  }

  ngOnInit() {
    // Initialization logic that requires the component to be fully set up
    this.data = 'Hello, World!';
    console.log('ngOnInit called');
  }
}

In this example, the constructor is used for basic setup, while ngOnInit is used to initialize the data property after the component is fully initialized.

Summary

  • Use a constructor for setting up dependencies and initializing properties.
  • Use ngOnInit for any logic that needs Angular to finish setting up the component (like input bindings or DOM access).

✅ Rule of Thumb: “Keep constructor light, move logic to ngOnInit().

CTEs (Common Table Expression)

Common Table Expressions (CTEs) are one of the most powerful and readable features in SQL. They help break down complex queries, improve code readability, and allow the use of recursion. In technical interviews, especially with top companies like Deloitte, TCS, Infosys, and Accenture, CTE-related questions are frequently asked to assess your problem-solving skills and query structuring ability.

What is a CTE?

A CTE (Common Table Expression) is a temporary result set that you can reference within a SELECT, INSERT, UPDATE, or DELETE statement. It is defined using the WITH keyword.

Syntax of a CTE

WITH CTE_Name AS (
    -- Your SQL query here
    SELECT column1, column2
    FROM TableName
    WHERE condition
)
SELECT * FROM CTE_Name;

Why Use CTEs?

  • To simplify complex joins and subqueries
  • Improve readability and maintainability
  • Enable recursive operations (e.g., hierarchical data)
  • Can be self-referenced in recursive CTEs
Example 1: Basic CTE to Simplify a Query

Problem: Get employees whose salary is above average.

WITH AvgSalaryCTE AS (
  SELECT AVG(Salary) AS AvgSal FROM Employees
)
SELECT e.Name, e.Salary
FROM Employees e, AvgSalaryCTE a
WHERE e.Salary > a.AvgSal;
✅ Instead of writing the AVG as a subquery, we make the query more readable.

Example 2: Recursive CTE to Handle Hierarchies

Problem: Get a hierarchical list of employees reporting to a manager
WITH EmployeeHierarchy AS (
  SELECT EmployeeID, Name, ManagerID, 0 AS Level
  FROM Employees
  WHERE ManagerID IS NULL

  UNION ALL

  SELECT e.EmployeeID, e.Name, e.ManagerID, eh.Level + 1
  FROM Employees e
  JOIN EmployeeHierarchy eh ON e.ManagerID = eh.EmployeeID
)
SELECT * FROM EmployeeHierarchy;
✅ This is especially useful for displaying org charts or processing file system structures.

Example 3: Find the Second-Highest Salary.
Using DISTINCT and ORDER BY with OFFSET (SQL Server / PostgreSQL):
SELECT DISTINCT Salary
FROM Employees
ORDER BY Salary DESC
OFFSET 1 ROW FETCH NEXT 1 ROW ONLY;

Using ROW_NUMBER() (works in most RDBMS):
WITH RankedSalaryCTE AS (
  SELECT Name, Salary
    ROW_NUMBER() OVER (ORDER BY Salary DESC) AS Rowum
FROM Employees
)
SELECT Name, Salary
FROM RankedSalaryCTE
WHERE RowNum = 2;

Example 4: Write a Query to Get All EVEN Position Records.
WITH NumberedRows AS (
  SELECT *, ROW_NUMBER() OVER (ORDER BY ID) AS RowNum
  FROM Employees
)
SELECT *
FROM NumberedRows
WHERE RowNum % 2 = 0;

The ROW_NUMBER() that we have used in the above query is known as a WINDOW Function. Let's understand each of them with an example:

Window Function.

A Window Function performs a calculation across a set of rows that are related to the current row, without collapsing them into a single output row (unlike aggregate functions).

It operates over a “window” of rows defined by the OVER() clause, allowing you to perform operations like ranking, running totals, comparisons with previous/next rows, and more — all while keeping the original row structure.
 
Example: Let’s say you want to assign a row number to each employee based on their salary within each department:
SELECT Name, Department, Salary,
       ROW_NUMBER() OVER (PARTITION BY Department ORDER BY Salary DESC) AS RowNum
FROM Employees;
  • PARTITION BY Department: Groups data by department.
  • ORDER BY Salary DESC: Orders rows within each partition.
  • ROW_NUMBER(): Assigns a unique number starting from 1 in each group.

Common Window Functions:
  • ROW_NUMBER() – Unique sequence number
  • RANK() / DENSE_RANK() – Ranking with or without gaps
  • LAG() / LEAD() – Compare with previous/next row
  • SUM(), AVG(), COUNT() – Running totals over a window

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."

DON'T MISS

Nature, Health, Fitness
© all rights reserved
made with by AlgoLesson