
Introduction
A RESTful API is a web service that follows REST (Representational State Transfer) principles, making it easy to interact with through HTTP methods like GET, POST, PUT, and DELETE. In .NET Core 9, building a RESTful API with Dependency Injection (DI) makes your code modular, testable, and maintainable.
For my article, I will explain RESTful APIs and DI in an easy-to-understand way, showing you how to create and use them effectively in .NET Core 9.
What is a dependency injection?

Dependency Injection (DI) is a software design pattern that helps manage dependencies in an application. Instead of a class creating its dependencies, they are injected from an external source, making the code more maintainable, testable, and scalable.
Benefits of dependency injection
- Loose coupling: Classes depend on abstractions rather than concrete implementations.
- Improved testability: Dependencies can be easily replaced with mocks for unit testing.
- Better maintainability: Code is more modular and easier to extend.
Implementing dependency injection in .NET Core 9
In .NET Core, DI is built into the framework and managed via the IServiceProvider interface. It enables services to be registered in a central container and injected wherever needed.
Steps to use DI:
- Create an interface defining the dependency.
- Implement the interface in a class.
- Register the service in the DI container.
- Inject the service into the required classes.
Comparing without and with dependency injection
Without dependency injection
In a tightly coupled approach, dependencies are instantiated directly inside the controller, making it hard to test and maintain:
Code block
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
private readonly ProductService _productService;
public ProductsController()
{
_productService = new ProductService(); // Hardcoded dependency
}
[HttpGet]
public ActionResult<List<Product>> Get()
{
return _productService.GetProducts();
}
}
public class ProductService
{
private readonly ProductRepository _productRepository = new();
public List<Product> GetProducts()
{
return _productRepository.GetProducts();
}
}
public class ProductRepository
{
public List<Product> GetProducts() => new() { new Product { Id = 1, Name = "Laptop", Price = 999.99M } };
}
Problems with this approach:
- The ProductsController is tightly coupled to ProductService, which in turn is tightly coupled to ProductRepository.
- Any change in ProductRepository affects all dependent classes.
- Difficult to test because dependencies cannot be easily replaced.

With dependency injection
By using DI, we inject dependencies instead of creating them inside the controller.
Code Block
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService; // Injected dependency
}
[HttpGet]
public ActionResult<List<Product>> Get()
{
return _productService.GetProducts();
}
}
public interface IProductService
{
List<Product> GetProducts();
}
public class ProductService : IProductService
{
private readonly IProductRepository _productRepository;
public ProductService(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public List<Product> GetProducts() => _productRepository.GetProducts();
}
public interface IProductRepository
{
List<Product> GetProducts();
}
public class ProductRepository : IProductRepository
{
public List<Product> GetProducts() => new() { new Product { Id = 1, Name = "Laptop", Price = 999.99M } };
}
Registering dependencies in DI container
To use dependency injection, we must register services in Program.cs:
Code block
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services. AddScoped<IProductService, ProductService>();
var app = builder.Build();

In this case, you can replace the implementation of the ProductRepository very easily.
First you need to create a new implementation
Code block
public class AlternateProductRepository : IProductRepository
{
public List<Product> GetProducts() => new() { new Product { Id = 2, Name = "Smartphone", Price = 799.99M } };
}
Second, replace the registration of old implementation in DI container (Program.cs
)
Code block
var builder = WebApplication.CreateBuilder(args);
// Replace the original implementation
builder.Services.AddScoped<IProductRepository, AlternateProductRepository>();
builder.Services. AddScoped<IProductService, ProductService>();
var app = builder.Build();
Thirdly, use the new implementation: Since we are injecting IProductRepository, the ProductService will automatically receive the new implementation without any changes in the code.
Unit test
Let’s see how to Dependency Injection in Unit Testing: You can use mocking to replace actual implementation of the class with external libraries, like Moq.
Step one: Install Moq
Code block
dotnet add package Moq
Step two: Create a Unit Test for Products Controller
Code block
using Moq;
using Xunit;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
public class ProductsControllerTests
{
[Fact]
public void Get_ReturnsProductList()
{
// Arrange
var mockService = new Mock<IProductService>();
mockService.Setup(service => service.GetProducts()).Returns(new List<Product>
{
new Product { Id = 1, Name = "Laptop", Price = 999.99M },
new Product { Id = 2, Name = "Smartphone", Price = 799.99M }
});
var controller = new ProductsController(mockService.Object);
// Act
var result = controller.Get();
// Assert
var okResult = Assert.IsType<ActionResult<List<Product>>>(result);
var productList = Assert.IsType<List<Product>>(okResult.Value);
Assert.Equal(2, productList.Count);
}
}
Step three: Mock the Repository Layer (For Testing Service)
Code block
public class ProductServiceTests
{
[Fact]
public void GetProducts_ReturnsProductList()
{
// Arrange
var mockRepo = new Mock<IProductRepository>();
mockRepo.Setup(repo => repo.GetProducts()).Returns(new List<Product>
{
new Product { Id = 1, Name = "Tablet", Price = 499.99M }
});
var service = new ProductService(mockRepo.Object);
// Act
var result = service.GetProducts();
// Assert
Assert.Single(result);
Assert.Equal("Tablet", result[0].Name);
}
}
You can see Dependency Injection helps:
- Easier Testing: Replace real implementation with mock data.
- No need actual database call: Fake data can be used
- Faster execution: external dependencies are removed.
Conclusion
Using Dependency Injection in .NET Core 9 makes applications more flexible, testable, and maintainable. By comparing both approaches, it’s clear that DI helps in reducing tight coupling, making it a best practice for building scalable applications.
However, when you want to scale some projects, even using .NET Core 9 can only help so much, then you need to speak to zen8labs. We have the skills to help scale up anything and allowing us to create something awesome together!
Chien Nguyen, Software Engineer