• Blog
  • Dependency Injection in ASP.NET Core: Building a Food Delivery System

Dependency Injection in ASP.NET Core: Building a Food Delivery System

Decouple, configure, and test your .NET Core apps with ease

Publish date:
Discover more of what matters to you

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:

  1. Construction Injection. It’s the most commonly used and recommended approach.
  2. Method Injection. Dependencies are passed as parameters to specific methods.
  3. Property Injection. Dependencies are set after object instantiation via public properties.
Accelerated .NET Migration
Our AI-assisted migration approach accelerates your .NET migration, reducing manual effort and keeping your architecture clean.
See more

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:

1
builder.Services.AddScoped<INotificationService, EmailNotificationService>();

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:

1
builder.Services.AddScoped<IDeliveryService, DeliveryService>();

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:

  1. Create an xUnit-based test project.
  2. Navigate to the project’s directory.
  3. Install the Moq Nuget package. Moq is a popular library for creating mock objects in .NET tests:
1
dotnet add package Moq

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.

Modernize Your Legacy Code with Clean .NET Architecture
Migrating from .NET Framework? Softacom will modernize your backend to match your business logic.
Talk to Experts

Subscribe to our newsletter and get amazing content right in your inbox.

This field is required
This field is required Invalid email address
By submitting data, I agree to the Privacy Policy

Thank you for subscribing!
See you soon... in your inbox!

confirm your subscription, make sure to check your promotions/spam folder

Get in touch
Our benefits
  • 17+ years of expertise in legacy software modernization
  • AI Migration Tool:
    faster timelines, lower costs, better accuracy (99.9%)
  • Accelerate release cycles by 30–50% compared to manual migration
  • 1–2 business day turnaround for detailed estimates
  • Trusted by clients across the USA, UK, Germany, and other European countries
Review
Thanks to Softacom's efforts, the solutions they delivered are already in use and have increased revenue streams.
  • Niels Thomassen
  • Microcom A/S
This field is required
This field is required Invalid email address Invalid business email address
This field is required
By submitting data, I agree to the Privacy Policy
We’ll reply within 24 hours — no sales talk, no spam