LEARN BUILD GROW TOGETHER
Practical knowledge for developers
DEVELOPER LEARNING HUB
Learn. Build. Grow Together.

Practical tutorials, real-world examples and interview preparation.

.NETC#SQL SERVERANGULAR
Showing posts with label Service Lifetime. Show all posts

Dependency Injection in .NET Core Web API – A Practical Guide with C# Examples

 

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
EmployeeService

In this example:

  • EmployeesController depends on IEmployeeService.

  • EmployeeService implements IEmployeeService.

  • The DI container provides an instance of EmployeeService when the controller requests IEmployeeService.

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 InjectionInversion of Control
A technique for supplying dependenciesA broader design principle
Commonly implemented through constructorsCan be implemented through different mechanisms
Helps reduce tight couplingTransfers 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:

  1. Transient

  2. Scoped

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

LifetimeInstance behaviorTypical use
TransientNew instance for each resolutionLightweight services
ScopedOne instance per scopeWeb request services
SingletonOne instance per containerShared 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.csproj

Each class has a clear responsibility.

ComponentResponsibility
ControllerHandles HTTP requests and responses
InterfaceDefines the service contract
ServiceContains employee business operations
ModelRepresents employee data
Program.csRegisters 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/employees

Expected 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/101

Expected response: HTTP 200 OK with employee details.

Create an employee

POST /api/employees

Request 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:

  1. Loose coupling: Classes depend on abstractions rather than creating concrete dependencies themselves.

  2. Maintainability: Responsibilities can be separated into smaller classes.

  3. Testability: Dependencies can be replaced with mock or fake implementations during unit testing.

  4. Reusability: Services can be reused by multiple controllers or other services.

  5. Centralized configuration: Service registrations and lifetimes can be managed in one place.

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