Archives 06/08/2024

Revisiting the Strategy pattern – Dependency Injection

In this section, we leverage the Strategy pattern to compose complex object trees and use DI to dynamically create those instances without using the new keyword, moving away from being control freaks and toward writing DI-ready code.The Strategy pattern is a behavioral design pattern we can use to compose object trees at runtime, allowing extra flexibility and control over objects’ behavior. Composing our objects using the Strategy pattern makes our classes smaller, easier to test and maintain, and puts us on the SOLID path.From now on, we want to compose objects and lower the amount of inheritance to a minimum. We call that principle composition over inheritance. The goal is to inject dependencies (composition) into the current class instead of depending on base class features (inheritance). Additionally, this approach enables us to pull out behaviors and place them in separate classes, adhering to the Single Responsibility Principle (SRP) and Interface Segregation Principle (ISP). We can reuse these behaviors in one or more different classes through their interface, embodying the Dependency Inversion Principle (DIP). This strategy promotes code reuse and composition.The following list covers the most popular ways of injecting dependencies into objects, allowing us to control their behaviors from the outside by composing our objects:

  • Constructor injection
  • Property injection
  • Method injection

We can also get dependencies directly from the container. This is known as the Service Locator (anti-)pattern. We explore the Service Locator pattern later in this chapter.

Let’s look at some theory and then jump into the code to see DI in action.

Constructor injection

Constructor injection consists of injecting dependencies into the constructor as parameters. This is the most popular and preferred technique by far. Constructor injection is useful for injecting required dependencies; you can add null checks to ensure that, also known as the guard clause (see the Adding a guard clause section).

Property injection

The built-in IoC container does not support property injection out of the box. The concept is to inject optional dependencies into properties. Most of the time, you want to avoid doing this because property injection leads to optional dependencies, leading to nullable properties, more null checks, and often avoidable code complexity. So when we think about it, it is good that ASP.NET Core left this one out of the built-in container.You can usually remove the property injection requirements by reworking your design, leading to a better design. If you cannot avoid using property injection, use a third-party container or find a way to build the dependency tree yourself (maybe leveraging one of the Factory patterns).Nevertheless, from a high-level view, the container would do something like this:

  1. Create a new instance of the class and inject all required dependencies into the constructor.
  2. Find extension points by scanning properties (this could be attributes, contextual bindings, or something else).
  3. For each extension point, inject (set) a dependency, leaving unconfigured properties untouched, hence its definition of an optional dependency.

There are a couple of exceptions to the previous statement regarding the lack of support:

  • Razor components (Blazor) support property injection using the [Inject] attribute.
  • Razor contains the @inject directive, which generates a property to hold a dependency (ASP.NET Core manages to inject it).

We can’t call that property injection per se because they are not optional but required, and the @inject directive is more about generating code than doing DI. They are more about an internal workaround than “real” property injection. That is as close as .NET gets from property injection.

I recommend aiming for constructor injection instead. Not having property injection should not cause you any problems. Often, our need for property injection stems from less-than-optimal design choices, whether from our design strategies or a framework we’re utilizing.

Next, we look at method injection.