Exploring the Dependency Injection Design Pattern and Its Support in ASP.NET Core

What is a dependency?

In general terms, a dependency is something that is required in order for something else to function or exist.

In the context of software development, dependencies refer specifically to the code artifacts that a module or component relies on in order to function properly. However, the concept of dependency is much broader and can apply to many different situations and contexts.

Can there be a system with no dependency?

It is generally not possible to completely eliminate dependencies among objects in a system. Even in a system with very simple objects, there will typically be some level of dependency.

Should we focus to minimize dependency?

Yes, we should minimize dependencies among objects in a system by following good design principles and using techniques such as dependency injection. This can help to make the system more modular, flexible, and maintainable.

Dependency injection is a design pattern that allows a client object to receive its dependencies from an external source rather than creating them itself.

Consider a typical e-commerce example as shown in the picture

1. Payment service to handle payment of order.

    public class PaymentService
    {
        public PaymentService()
        {
            ... ...
        }
        public void ChargePayment(Order order)
        {
            ... ...
        }
    }

2. Inventory service to handle product records according to order.

 public class InventoryService
    {
        public InventoryService()
        {
            ... ...
        }
        public void DecreaseProduct(Order order)
        {
            ... ...
        }
    }

3. Shipping service to handle the shipment of products according to the order.

    public class ShippingService
    {
        public ShippingService()
        {
            ... ... 
        }
        public void ShipOrder(Order order)
        {
            ... ...
        }
    }

The Order Processor of the e-commerce system uses the above services to complete the order placed by the user. The above services can be called as follows:

public class OrderProcessor
{

    public OrderProcessor()
    {
        ... ...
    }

    public void ProcessOrder(Order order)
    {
        var paymentService = new PaymentService();
        var inventoryService = new InventoryService();
        var shippingService = new ShippingService();

        paymentService.ChargePayment(order);
        
        inventoryService.DecreaseProduct(order);
        
        shippingService.ShipOrder(order.ShippingAddress);
    }
}

In the above code, we directly create objects of those services and call their methods to complete the action. This might do the job, but this creates a tight couple between the classes. The OrderProcessor is tightly coupled to these specific implementations of the dependencies and cannot be easily tested or used with different implementations.

To decouple the OrderProcessor from its dependencies and make it more testable and flexible, we can use dependency injection to pass the dependencies into the OrderProcessor via the constructor or setter methods. Here’s how the OrderProcessor Class would look with dependency injection using the constructor:

public class OrderProcessor
{
    private IInventoryService inventoryService;
    private IShippingService shippingService;
    private IPaymentService paymentService;

    public OrderProcessor(IInventoryService inventoryService,
                          IShippingService shippingService,
                          IPaymentService paymentService)
    {
        this.inventoryService = inventoryService;
        this.shippingService = shippingService;
        this.paymentService = paymentService;
    }

    public void ProcessOrder(Order order)
    {
        paymentService.ChargePayment(order);
        
        inventoryService.DecreaseProduct(order);
        
        shippingService.ShipOrder(order);
    }
}

To use the OrderProcessor with constructor injection, you would create a new instance of the OrderProcessor and pass in the dependencies as arguments to the constructor:

IInventoryService inventoryService = new InventoryService();
IShippingService shippingService = new ShippingService();
IPaymentService paymentService = new PaymentService();

OrderProcessor processor = new OrderProcessor(inventoryService, shippingService, paymentService);

.NET Core provides a built-in dependency injection (DI) container that can be used to manage the dependencies of your application. Using the DI container can make it easier to manage the dependencies of your application and can make your code more testable and flexible.

Here’s an example of how you could use the .NET Core DI container to manage the dependencies of the OrderProcessor class from the previous examples:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IInventoryService, InventoryService>();
    services.AddTransient<IShippingService, ShippingService>();
    services.AddTransient<IPaymentService, PaymentService>();
    services.AddTransient<OrderProcessor>();
} 

This code registers the dependencies with the DI container and specifies that a new instance of each dependency should be created every time it is requested.
Note: we will learn about service collection and lifetime in the next article.

public class OrdersController : ControllerBase
{
   private OrderProcessor processor;
   
   public OrdersController(OrderProcessor processor){
      this.processor = processor;
   }
   
   [HttpPost]
   public IActionResult ProcessOrder([FromBody Order order])
   {
      try
      {
        processor.ProcessOrder(order);
        return Ok();
      }
      catch(Exception ex)
      {
         return BadRequest(ex.Message);
      }
   }
}   

In this example, the DI container creates an instance of the OrderProcessor and injects it into the OrdersController via the constructor. The OrderProcessor dependencies are also created and injected by the DI container.

Using a .NET DI container can make it easier to manage the dependencies of your application and can make your code more testable and flexible, as you can easily swap out the dependencies with different implementations or use mock dependencies when testing.

Know more about service lifetime in the DI Container of ASP.NET Core:

Leave a Reply

Your email address will not be published. Required fields are marked *