Introduction

Using a dependency injection container, we can avoid manually implementing dependency injection. A dependency injection container is an object that knows how to instantiate and configure objects. To do this, it needs to know about the constructor arguments and the relationships between the classes.

This lesson will demonstrate that test code remains largely the same irrespective of whether the loosely coupled application code uses manual DI or a DI container.

Brief overview of DI containers

A DI container is a library that provides DI facilities. It simplifies object composition and service lifetime management by resolving and managing object dependencies.

DI containers can analyze the requested class and determine the required dependencies at runtime. There are numerous third-party DI container frameworks, in addition to Microsoft’s own. Here, we’ll use the built-in Microsoft.Extensions.DependencyInjection library.

The steps involved in implementing DI using Microsoft’s built-in DI container are to create a service collection and add services to the collection. After adding a service provider, we can access services according to some predefined scope.

Let’s see how we can use the Microsoft.Extensions.DependencyInjection DI container framework in the implementation of a loosely coupled BankAccount class and how this facilitates unit testing.

An example of using a DI container

Suppose we have a BankAccount class that includes an Amount field. The Withdraw and Deposit methods contained in the BankAccountService class allow for withdrawing and depositing funds to and from the specified bank account. This class is designed to only enable withdrawals on weekdays. To achieve this business rule, the DayChecker class is used, which returns whether or not the transaction date is on a weekday. The bank account is initialized with a balance of 1000.

The ConsoleApplication class has an IBankAccountService dependency and invokes its Withdraw method. The ConsoleApplication code is called from the code contained in the Program class (line 14) in Program.cs.

The Program class is where all the DI container work is included. Let’s explain some of the code in Program.cs:

  • RegisterServices: It registers all the dependencies. In this case, we tell the application to inject the BankAccountService and DayChecker dependencies (line 12) in Program.cs.

  • AddSingleton<T>: This method creates a new service of type T with a lifetime of singleton (lines 2123). Scope lifetime can be a singleton, scoped, or transient.

    Although a detailed understanding of these types is outside the scope of this course, a brief outline is provided below. Singleton, scoped and transient objects are best understood in the context of web applications where a client (for example, a web browser) calls the server. The controller handles the requests from the client and the service that manages this request can be one of the three aforementioned types.

    • A singleton object is instantiated just once throughout the web application. Its instance is created for the first request and reused in all subsequent requests to the server.
    • A scoped lifetime service is created once per HTTP request (which is a call from the client to the server). The same instance is then reused in other calls within the same HTTP request.
    • In a transient service, a new instance of the service is created with each new HTTP request.
  • DisposeServices: This is called when the dependencies are no longer required (line 15), so that the services we had previously built may be disposed of before program execution ends.

  • GetRequiredService<T>: This gets the service of type T from the IServiceProvider (line 14).

The exact same program can be written without the use of a DI container, which is found in the previous lesson. However, in order to use a DI container, the design pattern must change slightly in order to separate the runtime data into another class from the class that performs actions on runtime data. In this example, the state of the BankAccount (runtime data) is kept separate from the class BankAccountService, which contains methods which perform changes to this runtime data.

Sometimes when we create dependencies, include them in the DI container and define their lifetime, we are doing these actions on services. They are often called services because they service specific functionalities, for example, shipping, payment, and cart storage in a web application.

The resulting application using the built-in Microsoft DI container is shown below. Running the application below outputs the resulting bank balance from an initial balance of 1000and a withdrawal of 300.

Get hands-on with 1200+ tech skills courses.