When we talk about modularity and maintainability, we should have in mind the Dependency Injection (DI). ASP.NET Core comes with a built-in dependency injection system that helps us achieve Inversion of Control (IoC) between classes and their dependencies. This design pattern facilitates decoupling from implementations, allowing for greater flexibility without tightly coupling to specific dependencies.
Dependency Injection in ASP.NET Core
.NET Core provides a built-in container that allows registering and resolving dependencies. Instead of manually creating them, the IoC container injects them into the class, enhancing the modularity and maintainability of the general architecture. There are 3 main service lifetimes that we have to remember when we want to register them:
- Transient. A new instance is created every time it is requested. Best for lightweight, stateless services.
- Scoped. A single instance is created per request. Used for request-based dependencies.
- Singleton. A single instance is created and shared throughout the application’s lifetime. Used for expensive resources.
There are 3 common ways to inject dependencies into a class:
- Construction Injection. It’s the most commonly used and recommended approach.
- Method Injection. Dependencies are passed as parameters to specific methods.
- Property Injection. Dependencies are set after object instantiation via public properties.
Building a Food Delivery System with Dependency Injection in ASP.NET Core
To leverage the benefits, let’s start by registering the necessary services of our app in the DI container of .NET Core 8 Web API. The food delivery system consists of 3 main services:
- OrderService – Customers order food from restaurants.
- DeliveryService – Drivers pick up and deliver the orders.
- NotificationService – Customers receive updates about their orders.
Implementing the Notification Service
Let’s start with the notification service:
| 1234567891011 | public interface INotificationService
{
void SendNotification(string customerName, string message);
}
public class EmailNotificationService : INotificationService
{
public void SendNotification(string recipient, string message)
{
Console.WriteLine($"Email sent to {recipient}: {message}");
}
}
|
For simplicity, both the interface and the implementation are in the same file. The interface provides an abstraction for sending notifications, allowing multiple implementations (Email, SMS, Push). This way we follow the Open-Closed Principle, enabling new notification types to be added without modifying dependent classes. We can easily replace it with any other notification service type, just by creating a new service class, without modifying any dependent class.
In program.cs file, register the service in the dependency injection (DI) container:
Implementing the Delivery Service
Next, the delivery service assigns a driver to a customer’s order:
| 12345678910111213141516171819 | public interface IDeliveryService
{
string AssignDriver(string customerName);
}
public class DeliveryService : IDeliveryService
{
private readonly List<string> _availableDrivers = new() { "Alice", "Bob", "Charlie" };
private readonly Random _random = new();
public string AssignDriver(string customerName)
{
if (_availableDrivers.Count == 0)
{
return "No available drivers. Order is pending.";
}
string assignedDriver = _availableDrivers[_random.Next(_availableDrivers.Count)];
_availableDrivers.Remove(assignedDriver);
return $"Driver {assignedDriver} assigned to {customerName}'s order.";
}
}
|
It’s a key component of our food delivery system. We can later expand the behavior of the service by fetching real drivers from the database or by implementing delivery prioritization. By abstracting this functionality, we ensure flexibility and maintainability.
Again in the program.cs file, register the service so that it can be injected where needed:
Implementing the Order Service
The order service is responsible for placing orders. It depends on the other 2 services that we have already created:
| 1234567891011121314151617 | public interface IOrderService
{
string PlaceOrder(string customerName, string foodItem);
}
public class OrderService(IDeliveryService deliveryService, INotificationService notificationService)
: IOrderService
{
private readonly IDeliveryService _deliveryService = deliveryService;
private readonly INotificationService _notificationService = notificationService;
public string PlaceOrder(string customerName, string foodItem)
{
var deliveryMessage = _deliveryService.AssignDriver(customerName);
_notificationService.SendNotification(customerName,
$"Your order for {foodItem} is confirmed! {deliveryMessage}");
return $"Order placed for {foodItem} by {customerName}. {deliveryMessage}";
}
}
|
As you can see, we injected the abstractions of IDeliveryService and INotificationService in the constructor. The ASP.NET Core’s Dependency Injection (DI) container automatically resolves and constructs the necessary services, only when needed for the first time, ensuring efficient resource usage.
The same way to register the service in the program.cs file:
| 123 | builder.Services.AddScoped<INotificationService, EmailNotificationService>();
builder.Services.AddScoped<IDeliveryService, DeliveryService>();
builder.Services.AddScoped<IOrderService, OrderService>();
|
Why Scope for Our Services?
Within the same HTTP request, which is the scope, we want consistency and a new instance of OrderService, DeliveryService, and NotificationService. The same instance is used across multiple dependencies.
Creating the Order Controller
It’s time to create the entry point for handling the order requests, OrderController. First, we need a Data Transfer Object (DTO) to send a request for an order to the ASP.NET Core API:
| 123456 | public class OrderRequest
{
public required string CustomerName { get; set; }
public required string FoodItem { get; set; }
}
|
Then, the implementation of the Controller:
| 1234567891011121314 | public class OrderController : ControllerBase
{
private readonly IOrderService _orderService;
public OrderController(IOrderService orderService)
{
_orderService = orderService;
}
[HttpPost]
public IActionResult PlaceOrder([FromBody] OrderRequest request)
{
var result = _orderService.PlaceOrder(request.CustomerName, request.FoodItem);
return Ok(result);
}
}
|
It utilizes Dependency Injection(DI) to interact with the IOrderService, which is responsible for managing the orders, including driver assignment and customer notification. There is a clear separation between the Controller and the Service layer, which encapsulates the business logic.
Unit Testing the Implementation
Testing the implementation is more straightforward. Dependency injection allows us to mock the dependencies of the OrderService. One of the benefits of implementing DI is how it simplifies the process of testing, placing an order in our case, where we can mock the assignment of the driver and email notification. Follow these steps to create a test project:
- Create an xUnit-based test project.
- Navigate to the project’s directory.
- Install the Moq Nuget package. Moq is a popular library for creating mock objects in .NET tests:
Here’s how you can test the PlaceOrder method using xUnit and Moq:
| 12345678910111213 | [Fact]
public void PlaceOrder_Should_AssignDriver_And_SendNotification()
{
var mockDeliveryService = new Mock<IDeliveryService>();
var mockNotificationService = new Mock<INotificationService>();
mockDeliveryService.Setup(d => d.AssignDriver(It.IsAny<string>()))
.Returns("Driver assigned");
var orderService = new OrderService(mockDeliveryService.Object, mockNotificationService.Object);
var result = orderService.PlaceOrder("John", "Pizza");
var expectedMessage = "Order placed for Pizza by John. Driver assigned";
Assert.Equal(expectedMessage, result);
mockNotificationService.Verify(n => n.SendNotification("John", It.IsAny<string>()), Times.Once);
}
|
This test follows the pattern Arrange-Act-Assert(AAA). First, we create the mock instances and we configure the AssignDriver method to return “Driver assigned” when called with any string.
Next, we inject mocks into the OrderService, replacing the real dependencies. Finally, we call the PlaceOrder method and evaluate the expected results, which is the message when the order is placed successfully.
Conclusion
So, what is dependency injection in ASP.NET Core? It’s an implementation of IoC that decouples your classes from concrete implementations. It helps towards loosely coupled and easily extendable services, implementing the Inversion of Control (IoC) design pattern.
By applying the techniques described in this article, we can confidently build applications with clean code, strong separation of concerns, and enhanced maintainability.