Introduction
Imagine that we are developing an Employee Management API.
Initially, our controller may contain all the logic for retrieving, creating, updating, and deleting employees.
For a small application, this might seem manageable. But as the application grows, we may need to add:
Database operations
Email notifications
Employee validation
Logging
External API integrations
Business rules
If we keep adding all this logic directly to the controller, it becomes difficult to maintain, test, and reuse.
A better approach is to separate responsibilities into different classes.
For example:
Controller handles HTTP requests and responses.
Service contains business logic.
Repository handles data access.
But how does the controller get an instance of the service?
We could create it manually using the new keyword. However, this creates tight coupling between classes.
.NET provides a built-in mechanism called Dependency Injection (DI) to solve this problem.
In this article, we will understand DI step by step and implement it using our Employee Management API.
1. What Is Dependency Injection?
Dependency Injection is a design technique in which an object receives the other objects it needs instead of creating them itself.
Let's understand this with a simple example.
Suppose we have an EmployeeController that needs an EmployeeService.
Without Dependency Injection:
public class EmployeesController : ControllerBase
{
private readonly EmployeeService _employeeService;
public EmployeesController()
{
_employeeService = new EmployeeService();
}
}Here, the controller creates the EmployeeService object directly.
This creates tight coupling because the controller is responsible for creating its dependency.
Now consider the DI approach:
public class EmployeesController : ControllerBase
{
private readonly IEmployeeService _employeeService;
public EmployeesController(IEmployeeService employeeService)
{
_employeeService = employeeService;
}
}Here, the controller does not create the service.
Instead, it receives the required service through its constructor.
The .NET Dependency Injection container creates and provides the appropriate implementation.
The key idea: A class should focus on its own responsibility rather than managing the creation of its dependencies.
2. What Is a Dependency?
A dependency is an object or service that another class requires to perform its work.
For example:
EmployeesController
|
| Depends on
v
IEmployeeService
|
| Implemented by
v
EmployeeServiceIn this example:
EmployeesControllerdepends onIEmployeeService.EmployeeServiceimplementsIEmployeeService.The DI container provides an instance of
EmployeeServicewhen the controller requestsIEmployeeService.
This makes the application easier to extend and test.
3. What Is Inversion of Control (IoC)?
Inversion of Control is a broader design principle in which the responsibility for controlling object creation or other application flow is transferred to an external component or framework.
Without IoC, a class may create and manage its dependencies itself.
With DI, the framework's container can manage object creation and provide dependencies.
For example:
public EmployeesController(IEmployeeService employeeService)
{
_employeeService = employeeService;
}The controller declares what it needs, while the DI container handles supplying the implementation.
DI vs IoC
| Dependency Injection | Inversion of Control |
|---|---|
| A technique for supplying dependencies | A broader design principle |
| Commonly implemented through constructors | Can be implemented through different mechanisms |
| Helps reduce tight coupling | Transfers control to another component |
Remember: Dependency Injection is one way to implement Inversion of Control.
4. Types of Dependency Injection
There are three commonly discussed forms of Dependency Injection.
A. Constructor Injection
Dependencies are provided through a class constructor.
public class EmployeeService
{
private readonly ILogger<EmployeeService> _logger;
public EmployeeService(ILogger<EmployeeService> logger)
{
_logger = logger;
}
}This is the most commonly used approach in .NET applications.
B. Property Injection
Dependencies are assigned through public properties.
public class EmployeeService
{
public ILogger<EmployeeService>? Logger { get; set; }
}The dependency must be assigned by some external mechanism.
The built-in .NET DI container does not automatically perform general property injection for ordinary services.
C. Method Injection
Dependencies are supplied through a method parameter.
public class EmployeeService
{
public void ProcessEmployee(ILogger<EmployeeService> logger)
{
logger.LogInformation("Processing employee.");
}
}Method injection is useful when a dependency is required only for a particular operation.
For most application services and controllers, constructor injection is the recommended default.
5. Understanding the Built-in DI Container in .NET
.NET provides a built-in Dependency Injection container.
We use it to:
Register services.
Configure service lifetimes.
Resolve dependencies.
Manage object creation and disposal.
Service registration generally happens in Program.cs.
For example:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<IEmployeeService, EmployeeService>();
var app = builder.Build();
app.MapControllers();
app.Run();Let's understand this line:
builder.Services.AddScoped<IEmployeeService, EmployeeService>();It tells the DI container:
Whenever an application component requests IEmployeeService, provide an instance of EmployeeService according to the Scoped lifetime.
The controller does not need to know how the service is created.
6. Understanding Service Lifetimes
One of the most important concepts in Dependency Injection is service lifetime.
.NET provides three commonly used service lifetimes:
Transient
Scoped
Singleton
Let's understand each one.
A. Transient
A new instance is created each time the service is requested from the container.
Registration:
builder.Services.AddTransient<IEmployeeService, EmployeeService>();Example:
public class EmployeeService : IEmployeeService
{
}If two separate components request the transient service, they receive different instances.
Common use cases:
Lightweight stateless services.
Small utility services.
Operations that do not need shared instance state.
B. Scoped
A single instance is created for each dependency injection scope.
In a typical ASP.NET Core Web API request, scoped services are shared within that request. A new request gets a different scoped instance.
Registration:
builder.Services.AddScoped<IEmployeeService, EmployeeService>();Common use cases:
Business services.
Entity Framework Core
DbContext.Repository services that use a scoped DbContext.
This is a common lifetime for services used in Web APIs.
C. Singleton
A single instance is created and reused for the lifetime of the DI container.
Registration:
builder.Services.AddSingleton<IEmployeeService, EmployeeService>();Common use cases:
Thread-safe shared services.
Application-wide caches.
Services that maintain shared, safely managed state.
A singleton should not directly depend on a scoped service because the scoped dependency cannot safely be used as a longer-lived object.
Service lifetime comparison
| Lifetime | Instance behavior | Typical use |
|---|---|---|
| Transient | New instance for each resolution | Lightweight services |
| Scoped | One instance per scope | Web request services |
| Singleton | One instance per container | Shared application services |
Practical lifetime example
Let's create a simple service to understand the difference.
public interface IOperation
{
Guid GetOperationId();
}
public class Operation : IOperation
{
private readonly Guid _id = Guid.NewGuid();
public Guid GetOperationId()
{
return _id;
}
}Register it as Scoped:
builder.Services.AddScoped<IOperation, Operation>();If two controllers or services resolve IOperation within the same request scope, they receive the same instance.
If another HTTP request comes in, it receives a new scoped instance.
Changing the registration to Transient or Singleton changes that behavior.
7. Practical Implementation: Dependency Injection in Employee Management API
Now let's implement DI in our existing Employee Management API.
In previous articles, we placed employee operations directly inside the controller.
We will now separate the business logic into a service.
Step 1: Create the Employee model
Create a folder named Models and add Employee.cs.
namespace EmployeeManagementAPI.Models;
public class Employee
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Department { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}Step 2: Create the service interface
Create a folder named Services.
Inside it, create IEmployeeService.cs.
using EmployeeManagementAPI.Models;
namespace EmployeeManagementAPI.Services;
public interface IEmployeeService
{
List<Employee> GetAllEmployees();
Employee? GetEmployeeById(int id);
Employee CreateEmployee(Employee employee);
}Why do we create an interface?
Because the controller should depend on an abstraction rather than a concrete implementation.
This makes it easier to replace the implementation later, for example, when we move from in-memory data to SQL Server.
Step 3: Create the EmployeeService implementation
Create EmployeeService.cs inside the Services folder.
using EmployeeManagementAPI.Models;
namespace EmployeeManagementAPI.Services;
public class EmployeeService : IEmployeeService
{
private static readonly List<Employee> Employees =
[
new Employee
{
Id = 101,
Name = "Rahul Sharma",
Department = "IT",
Email = "rahul@example.com"
},
new Employee
{
Id = 102,
Name = "Amit Verma",
Department = "Finance",
Email = "amit@example.com"
}
];
private static int _nextId = 103;
public List<Employee> GetAllEmployees()
{
return Employees;
}
public Employee? GetEmployeeById(int id)
{
return Employees.FirstOrDefault(e => e.Id == id);
}
public Employee CreateEmployee(Employee employee)
{
employee.Id = _nextId++;
Employees.Add(employee);
return employee;
}
}Here, we have moved employee-related operations into the service class.
The controller no longer needs to know how employees are stored or created.
Practical note: This example intentionally uses static in-memory data to keep the DI concept simple. It is not suitable for production. The collection is shared within the application process, is not persistent, and is not protected against concurrent modifications. We will introduce database-backed operations later.
Step 4: Register the service in Program.cs
Open Program.cs and register the service.
using EmployeeManagementAPI.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<IEmployeeService, EmployeeService>();
var app = builder.Build();
app.UseHttpsRedirection();
app.MapControllers();
app.Run();This registration connects the interface with its implementation.
Whenever a controller requests IEmployeeService, the container supplies EmployeeService.
Step 5: Inject the service into EmployeesController
Now create or update EmployeesController.cs.
using EmployeeManagementAPI.Models;
using EmployeeManagementAPI.Services;
using Microsoft.AspNetCore.Mvc;
namespace EmployeeManagementAPI.Controllers;
[ApiController]
[Route("api/[controller]")]
public class EmployeesController : ControllerBase
{
private readonly IEmployeeService _employeeService;
public EmployeesController(IEmployeeService employeeService)
{
_employeeService = employeeService;
}
[HttpGet]
public IActionResult GetAllEmployees()
{
var employees = _employeeService.GetAllEmployees();
return Ok(employees);
}
[HttpGet("{id:int}")]
public IActionResult GetEmployeeById(int id)
{
var employee = _employeeService.GetEmployeeById(id);
if (employee == null)
{
return NotFound();
}
return Ok(employee);
}
[HttpPost]
public IActionResult CreateEmployee(Employee employee)
{
var createdEmployee = _employeeService.CreateEmployee(employee);
return CreatedAtAction(
nameof(GetEmployeeById),
new { id = createdEmployee.Id },
createdEmployee
);
}
}Let's understand the important part:
public EmployeesController(IEmployeeService employeeService)
{
_employeeService = employeeService;
}We are using constructor injection.
The controller declares that it requires IEmployeeService.
The DI container resolves that interface using the registration in Program.cs and provides the EmployeeService implementation.
Notice that the controller no longer creates the service using new EmployeeService().
8. Understanding the Project Structure
After implementing DI, our project structure looks like this:
EmployeeManagementAPI
│
├── Controllers
│ └── EmployeesController.cs
│
├── Models
│ └── Employee.cs
│
├── Services
│ ├── IEmployeeService.cs
│ └── EmployeeService.cs
│
├── Program.cs
│
└── EmployeeManagementAPI.csprojEach class has a clear responsibility.
| Component | Responsibility |
|---|---|
| Controller | Handles HTTP requests and responses |
| Interface | Defines the service contract |
| Service | Contains employee business operations |
| Model | Represents employee data |
| Program.cs | Registers application dependencies |
This separation helps us maintain and extend the application as requirements grow.
9. Test the API Using Postman
Run the application and test the following endpoints.
Get all employees
GET /api/employeesExpected response:
[
{
"id": 101,
"name": "Rahul Sharma",
"department": "IT",
"email": "rahul@example.com"
},
{
"id": 102,
"name": "Amit Verma",
"department": "Finance",
"email": "amit@example.com"
}
]Get employee by ID
GET /api/employees/101Expected response: HTTP 200 OK with employee details.
Create an employee
POST /api/employeesRequest body:
{
"name": "Priya Singh",
"department": "HR",
"email": "priya@example.com"
}Expected response: HTTP 201 Created with the newly created employee.
10. Common Dependency Injection Mistakes
Mistake 1: Forgetting to register a service
Suppose the controller requests IEmployeeService, but we forget to register it in Program.cs.
The application will fail to resolve the dependency when creating the controller.
Correct registration:
builder.Services.AddScoped<IEmployeeService, EmployeeService>();Mistake 2: Using the wrong service lifetime
A common mistake is injecting a scoped service into a singleton service.
For example, a singleton service should not directly capture a scoped DbContext.
This can cause lifetime and state-management problems.
Mistake 3: Registering an interface with the wrong implementation
If we register the wrong class or forget to implement the interface, the application will not be configured correctly.
Always verify the interface and implementation match.
Mistake 4: Using Singleton for every service
Singleton does not mean better performance in every situation.
If a singleton stores mutable state, concurrent requests may access that state at the same time.
Choose a lifetime based on the service's responsibility and dependencies.
11. Benefits of Dependency Injection
Dependency Injection provides several advantages:
Loose coupling: Classes depend on abstractions rather than creating concrete dependencies themselves.
Maintainability: Responsibilities can be separated into smaller classes.
Testability: Dependencies can be replaced with mock or fake implementations during unit testing.
Reusability: Services can be reused by multiple controllers or other services.
Centralized configuration: Service registrations and lifetimes can be managed in one place.
Flexibility: Implementations can be changed without rewriting the controller.
For example, we can replace an in-memory EmployeeService with a SQL Server-backed implementation while keeping the controller dependent on the same interface.
12. Interview Questions
Q1. What is Dependency Injection?
Dependency Injection is a technique in which a class receives its dependencies from an external source instead of creating them internally.
Q2. What is Inversion of Control?
IoC is a design principle that transfers control of object creation or other behavior to an external component or framework.
Q3. What are the three service lifetimes in .NET?
Transient, Scoped, and Singleton.
Q4. What is the difference between Scoped and Singleton?
Scoped services are created once per scope, while Singleton services are shared for the lifetime of the DI container.
Q5. Which DI type is commonly used in .NET Core?
Constructor Injection is the commonly recommended approach for application services and controllers.
Q6. Why should we use interfaces with Dependency Injection?
Interfaces provide abstractions that reduce coupling and allow implementations to be replaced or substituted during testing.
Q7. Can a Singleton service depend on a Scoped service?
A singleton should not directly capture a scoped dependency because their lifetimes are different. If required, a new scope can be created using IServiceScopeFactory for a controlled operation.
Q8. Where do we register services in a .NET Core Web API?
Services are commonly registered using builder.Services in Program.cs.
Conclusion
Dependency Injection is an important part of modern .NET application development.
It helps us separate responsibilities, reduce tight coupling, and build applications that are easier to test and maintain.
In our Employee Management API, we moved employee operations from the controller into a separate service and used constructor injection to provide that service to the controller.
This is a small but important step toward building a properly structured application.
As our application grows, we can introduce repositories, database access, logging, validation, and other services without putting everything inside the controller.
What's Next?
Day 9 – DTOs and Model Validation in .NET Core Web API
In the next article, we will understand why we should not always expose database or entity models directly through API endpoints. We will create DTOs, validate incoming request data, and return meaningful validation errors.
Learn • Build • Grow Together
No comments:
Post a Comment