In this article, we’ll explain dependency injection and the IoC container by refactoring a very simple code example in C#.

Brief introduction:

Dependency injection and IoC may seem quite complex at first glance, but they are very easy to learn and understand.

In this article, we’ll explain dependency injection and the IoC container by refactoring a very simple code example in C#.

Requirements:

Build an application that allows users to view available products and search for them by name.

First attempt:

We’ll start by creating a layered architecture. There are several benefits to using a layered architecture, but we won’t list them in this article because we are focused on dependency injection.

Here is the class diagram for the application:

First, we’ll start by creating a Product class:

public class Product { public Guid Id { get; set; } public string Name { get; set; } public string Description { get; set; }}Copy the code

Then, we will create the data access layer:

public class ProductDAL { private readonly List<Product> _products; public ProductDAL() { _products = new List<Product> { new Product { Id = Guid.NewGuid(), Name= "iPhone 9", Description = "iPhone 9 mobile phone" }, new Product { Id = Guid.NewGuid(), Name= "iPhone X", Description = "iPhone X mobile phone" } }; } public IEnumerable<Product> GetProducts() { return _products; } public IEnumerable<Product> GetProducts(string name) { return _products .Where(p => p.Name.Contains(name)) .ToList(); }}Copy the code

Then, we will create the business layer:

public class ProductBL { private readonly ProductDAL _productDAL; public ProductBL() { _productDAL = new ProductDAL(); } public IEnumerable<Product> GetProducts() { return _productDAL.GetProducts(); } public IEnumerable<Product> GetProducts(string name) { return _productDAL.GetProducts(name); }}Copy the code

Finally, we’ll create the UI:

class Program { static void Main(string[] args) { ProductBL productBL = new ProductBL(); var products = productBL.GetProducts(); foreach (var product in products) { Console.WriteLine(product.Name); } Console.ReadKey(); }}Copy the code

The code we have written in our first attempt is a good work product, but there are a few problems:

1. We can’t have three different teams working on each level.

2. The business layer is difficult to scale because it depends on the implementation of the data access layer.

3. The business layer is difficult to maintain because it depends on the implementation of the data access layer.

4. Source code is hard to test.

Second attempt:

High-level objects should not depend on low-level objects. Both must rely on abstraction. So what are the abstractions?

Abstraction is the definition of functionality. In our example, the business layer relies on the data access layer to retrieve the books. In C#, we use interfaces for abstraction. Interfaces represent an abstraction of functionality.

Let’s create an abstraction.

Here is an abstraction of the data access layer:

public interface IProductDAL
{
    IEnumerable<Product> GetProducts();
    IEnumerable<Product> GetProducts(string name);
}
Copy the code

We also need to update the data access layer:

public class ProductDAL : IProductDAL
Copy the code

We also need to update the business layer. In effect, we will update the business layer to rely on the abstraction of the data access layer rather than the implementation of the data access layer:

public class ProductBL { private readonly IProductDAL _productDAL; public ProductBL() { _productDAL = new ProductDAL(); } public IEnumerable<Product> GetProducts() { return _productDAL.GetProducts(); } public IEnumerable<Product> GetProducts(string name) { return _productDAL.GetProducts(name); }}Copy the code

We must also create an abstraction for the business layer:

public interface IProductBL
{
    IEnumerable<Product> GetProducts();
    IEnumerable<Product> GetProducts(string name);
}
Copy the code

We also need to update the business layer:

public class ProductBL : IProductBL
Copy the code

Eventually we need to update the UI:

class Program { static void Main(string[] args) { IProductBL productBL = new ProductBL(); var products = productBL.GetProducts(); foreach (var product in products) { Console.WriteLine(product.Name); } Console.ReadKey(); }}Copy the code

The code we did in the second attempt works, but we still rely on the concrete implementation of the data access layer:

public ProductBL()
{
    _productDAL = new ProductDAL();
}
Copy the code

So, how to solve it?

This is where the dependency injection pattern comes in.

Finally try

So far, nothing we’ve done has had anything to do with dependency injection.

In order for a business layer at a higher level to rely on the functionality of lower-level objects without concrete implementation, someone else must create the class. Someone else must provide a concrete implementation of the underlying object, which is what we call dependency injection. Its literal meaning is that we inject dependent objects into higher-level objects. One way to implement dependency injection is to use constructors for dependency injection.

Let’s update the business layer:

public class ProductBL : IProductBL { private readonly IProductDAL _productDAL; public ProductBL(IProductDAL productDAL) { _productDAL = productDAL; } public IEnumerable<Product> GetProducts() { return _productDAL.GetProducts(); } public IEnumerable<Product> GetProducts(string name) { return _productDAL.GetProducts(name); }}Copy the code

The infrastructure must provide implementation dependencies:

class Program { static void Main(string[] args) { IProductBL productBL = new ProductBL(new ProductDAL()); var products = productBL.GetProducts(); foreach (var product in products) { Console.WriteLine(product.Name); } Console.ReadKey(); }}Copy the code

The controls that create the data access layer are aligned with the infrastructure. This is also known as inversion of control. Instead of creating an instance of the data access layer in the business layer, we create it in the infrastructure. The Main method will inject the instance into the business logic layer. Therefore, we inject an instance of a low-level object into an instance of a high-level object.

This is called dependency injection.

Now, if we look at the code, we rely only on the abstraction of the data access layer in the business access layer, which uses the interfaces implemented by the data access layer. Therefore, we follow the principle that both higher level objects and lower level objects depend on abstraction, which is the contract between higher level objects and lower level objects.

Now, we can have different teams working on different layers. We can have one team handling the data access layer, one team handling the business layer, and one team handling the UI.

Next, the benefits of maintainability and extensibility are shown. For example, if we want to create a new data access layer for SQL Server, we simply implement the abstraction of the data access layer and inject instances into the infrastructure.

Finally, the source code is now testable. Because we use interfaces everywhere, we can easily provide another implementation in lower unit tests. This means that lower tests will be easier to set up.

Now, let’s test the business layer.

We will use xUnit for unit testing and Moq to simulate the data access layer.

Here are the unit tests for the business layer:

public class ProductBLTest { private readonly List<Product> _products = new List<Product> { new Product { Id = Guid.NewGuid(), Name= "iPhone 9", Description = "iPhone 9 mobile phone" }, new Product { Id = Guid.NewGuid(), Name= "iPhone X", Description = "iPhone X mobile phone" } }; private readonly ProductBL _productBL; public ProductBLTest() { var mockProductDAL = new Mock<IProductDAL>(); mockProductDAL .Setup(dal => dal.GetProducts()) .Returns(_products); mockProductDAL .Setup(dal => dal.GetProducts(It.IsAny<string>())) .Returns<string>(name => _products.Where(p => p.Name.Contains(name)).ToList()); _productBL = new ProductBL(mockProductDAL.Object); } [Fact] public void GetProductsTest() { var products = _productBL.GetProducts(); Assert.Equal(2, products.Count()); } [Fact] public void SearchProductsTest() { var products = _productBL.GetProducts("X"); Assert.Single(products); }}Copy the code

As you can see, it’s easy to set up unit tests using dependency injection.

The IoC container

Containers are just things that help with dependency injection. Containers typically perform three different functions:

1. Register mappings between interfaces and implementations

2. Create objects and resolve dependencies

Release 3.

Let’s implement a simple container to register the mapping and create objects.

First, we need a data structure to store the mapping. We will choose Hashtable. This data structure stores the mapping.

First, we’ll initialize the Hashtable in the container’s constructor. We will then create a RegisterTransient method to register the mapping. Finally, we Create a method to Create the object:

public class Container { private readonly Hashtable _registrations; public Container() { _registrations = new Hashtable(); } public void RegisterTransient<TInterface, TImplementation>() { _registrations.Add(typeof(TInterface), typeof(TImplementation)); } public TInterface Create<TInterface>() { var typeOfImpl = (Type)_registrations[typeof(TInterface)]; if (typeOfImpl == null) { throw new ApplicationException($"Failed to resolve {typeof(TInterface).Name}"); } return (TInterface)Activator.CreateInstance(typeOfImpl); }}Copy the code

Eventually, we’ll update the UI:

class Program { static void Main(string[] args) { var container = new Container(); container.RegisterTransient<IProductDAL, ProductDAL>(); IProductBL productBL = new ProductBL(container.Create<IProductDAL>()); var products = productBL.GetProducts(); foreach (var product in products) { Console.WriteLine(product.Name); } Console.ReadKey(); }}Copy the code

Now, let’s implement the Resolve method in the container. This approach resolves dependencies.

The Resolve method is as follows:

public T Resolve<T>()
{
    var ctor = ((Type)_registrations[typeof(T)]).GetConstructors()[0];
    var dep = ctor.GetParameters()[0].ParameterType;
    var mi = typeof(Container).GetMethod("Create");
    var gm = mi.MakeGenericMethod(dep);
    return (T)ctor.Invoke(new object[] { gm.Invoke(this, null) });
}
Copy the code

Then we can use the following Resolve method in the UI:

class Program { static void Main(string[] args) { var container = new Container(); container.RegisterTransient<IProductDAL, ProductDAL>(); container.RegisterTransient<IProductBL, ProductBL>(); var productBL = container.Resolve<IProductBL>(); var products = productBL.GetProducts(); foreach (var product in products) { Console.WriteLine(product.Name); } Console.ReadKey(); }}Copy the code

In the source code above, the container uses the container.resolve () method to create an object of the ProductBL class. The ProductBL class is a dependency of IProductDAL. Therefore, container.resolve () returns an object of the ProductBL class by automatically creating and injecting a ProductDAL object into it. It’s all going on behind the scenes. The ProductDAL object is created and injected because we registered the ProductDAL type with IProductDAL.

This is a very simple and basic IoC container that shows you what’s behind the IoC container. That’s it. I hope you enjoyed reading this article.

Welcome to pay attention to my public number, if you have a favorite foreign language technical articles, you can recommend to me through the public number message.