June 18th, 2020 | by Konrad Zajda
Inversion of Control #1: Dependency Injection
Table of contents
Inversion of Control (IoC) is a paradigm of software development (although some call it a pattern) in object-oriented programming. Talking about IoC we mean inversion of the application’s code flow. We distinguish several implementations of the inversion of control: dependency injection, events, or aspect-oriented programming. We do it to achieve loose coupling.
Today, in the first article of the inversion of control series, we will take a closer look at the first implementation, which is dependency injection. Before we move on, let’s see how standard, non-inverted control looks like:
Figure 1. Standard, non-inverted control of the flow.
With the standard way, the Foo is the one responsible for creating its’ dependencies; it knows their number and their types. When we are initializing these dependencies directly, we can run into some unobvious troubles. Primarily, we tie Foo’s dependencies with exact implementations, which, from Foo’s perspective, are redundant. Foo does not need to know their types as far as they do their job. In that way, we create a coupling between modules of the different levels, and we base our classes on the specific types, instead of an abstraction. Also, when calling a constructor, we need to know its’ all required parameters. What about unit testing? Can we make sure we are testing Foo itself instead of testing both Foo, and Bar?
Dependency Injection
These (and many other) issues can be eliminated with injecting dependencies into our Foo, making sure it has everything it needs to run properly. We can inject them with the constructor or the method *itself.
Figure 2. Control inverted with the dependency injection.
If we inject interface over a specific type, we are not only creating a unit-testable class, but also replacing dependency’s implementation will not require any changes in the Foo.
Figure 3. Control inverted with the injection of dependency based on the interface.
Figure 4. Foo as a unit-testable class with Mock or Stub (NSubstitute here) injected.
After all, we have to initialize a specific type of the Bar before injecting it into the Foo. As far as we are talking about such a simple case, it’s quite easy, isn’t it? How about the application with thousands of classes? When to create a new object? Should I dispose of it? Can I inject the single dependency into multiple objects?
Figure 5. Inverted control and initialization of co-working objects.
*There are more ways to inject dependencies, but this article does not cover them.
Dependency Container in .NET Core
Because we can find out manual dependency injection quite problematic, we can consider* using dependency containers. These are making dependencies initialization and injection much easier, taking care of how, when, and to whom to inject the dependency. Notice that it’s an excellent example of the “inversion of control”, as we give control over the initialization and injection to the dependency container. There are tons of excellent implementations, like Ninject, Autofac or Unity. But, with the .NET Core, Microsoft has introduced their own container, which is the default for ASP.NET Core applications.
These dependencies are being called services in .NET Core, and that’s why we can see two main interfaces when we are creating such containers: IServiceCollection and IServiceProvider.
IServiceCollection is a collection that contains ServiceDescriptors, which describes service type, service implementation type, its’ factory (which is a function that returns that service), and its’ lifetime. Based on descriptors in the IServiceCollection, IServiceProvider can be built. It defines a mechanism for retrieving service objects (dependencies) from the container.
Figure 6. IServiceProvider building and retrieving Foo from it. It’s the same as Figure 5, but with a dependency container.
Service lifetime
Dependency container not only helps with constructing objects and injecting them but also allows us to manage the object’s lifetime more easily. In .NET Core DI, we can see three different service lifetimes:
- Transient – a new instance of the object will be created every time.
- Singleton – only one instance of the object will be created, which will be shared across all the services with that service injected into.
- Scoped – only one instance of the object will be created in a single scope, which will be shared across all the services within that scope.
In the following example, we are creating a unique Guid for every instance of the Foo:
Figure 7. Example of a transient service lifetime.
Figure 8. Example of a singleton service lifetime.
Figure 9. Example of a scoped service lifetime.
To simplify it, let’s say that scoped behaves like a singleton within a single service scope. But how can we use it?
In the ASP.NET Core WebApi2, for every incoming HTTP request, new service scope will be created. With the scoped service lifetime, we can create services that behave as a singleton across a single HTTP request—for example, a class which extracts and stores a unique identifier of the user.
Figure 10. Example of a scoped service lifetime usage.
Make it in a different way
Dependency Injection allows us to change the behavior of our program easily. As we can inject interfaces into our objects, we can also change dependencies’ implementations without modifying existing code.
Dependency Injection coupling
Dependency injection is a great way to prepare your code to do something in a different way as you give control of some part of your class to its’ dependencies. As I mentioned before, inversion of control is used to achieve loose coupling in our code. But with dependency injection, there is coupling too. Notice that in our examples, we always know how many dependencies we have and when they will return their results (if any). In this way, we are coupled not only with the well-known amount of the objects but also with the time itself.
However, sometimes, you don’t need to know how many objects you work with, what they do, and when (or even if they will) return some results. This kind of decoupling can be handled with events. Events allow us to achieve looser coupling, by decoupling from the time and amount of the objects you work with. They allow you not only to do something different but also to do something more without changing a single line of your code. We will cover events in the next article of the Inversion of Control series.