Dependency Injection in C#

Posted by : on

Category : C#   Architecture

What is Dependency Injection

Dependency injection is a programming technique that fosters the development of loosely coupled code.

Based on a set of programming principles emphasizing composition, dependency injection allows us to program to interfaces rather than concrete implementations.

By employing dependency injection, we can create code that is more maintainable and extensible. In the forthcoming post, I will discuss the benefits of using DI, the three different types of DI, common misapplications of DI that fail to achieve loose coupling, and why correctly implementing DI aids in writing better code across all programming levels, from low-level patterns like the strategy pattern to high level concepts like the Open Closed Principle

Firstly, let’s explore how DI contributes to our code architecture.

The Benefits of Dependency Injection

Dependency injection aids in six distinct aspects of our code.

Maintainability

The first and most well-known benefit is code maintainability. Each class’s responsibilities are clearly defined, making it easier to understand and reason about. Program changes are simplified because we only need to add new code rather than modify existing code. This application aligns with the Open Closed Principle, typically utilizing behavioral patterns like the strategy or command pattern. By injecting the appropriate implementation, either at compile-time or run-time, we can alter the program’s behavior without altering existing code.

Debugging

With clearly defined concrete implementations injected through interfaces, pinpointing bugs becomes more manageable. Since code is focused on specific, well-defined behaviors, locating bugs is simplified as we can narrow down the affected code area. Additionally, since code performs discrete tasks, debugging efforts are streamlined without altering previously written code.

Testing

DI benefits testing in two ways due to loose coupling. Firstly, it facilitates testing units of behavior by creating properly isolated code representing a specific behavior unit. Secondly, it simplifies dependency replacement in the tested code with fake dependencies tailored for testing purposes.

Extensibility

DI aids in accommodating program changes and the addition of new behaviors. New code can be created and injected at points beyond the class using it, allowing for flexible dependency injections originating from a single entry point. Moreover, existing classes’ behaviors can be extended using DI, employing structural modification patterns like the decorator, composite, or adapter patterns.

Parallel Programming

Loosely coupled code facilitated by DI enables multiple developers to work on the same codebase. Each developer or team can develop concrete implementations adhering to the interface, independent of other teams’ code. This promotes parallel development as each team can focus on code with strictly defined responsibilities.

Late Binding

Lastly, DI facilitates late binding, enabling behavior swapping during run-time or dynamic object instantiation from types unknown at compile time. This capability empowers scenarios where objects implementing interfaces are created using reflection based on types defined in configuration files read at run-time.

The Different Types Of Dependency Injection

There are three types of dependency injection: constructor injection, method injection and property injection. There is also the matter of when these injections are defined in our program.

Constructor Injection

Constructor injection is the most commonly used type of DI. It occurs when parameters are passed to the public constructor of a class.

With constructor injection, we furnish a starting state for our objects, which can also be immutable. This approach ensures that each class is provided with the dependencies it requires to function. If a class includes fields that rely on types lacking obvious default values, constructor injection enables us to supply the necessary initial values for those fields.

Here’s an example of constructor injection:

public class Enemy
{
   private readonly IAttackType _attackType;

   public Enemy(IAttackType attackType) => _attackType = attackType;

   public void Attack() => _attackType.Attack();
}

public interface IAttackType
{
   void Attack();
}

public class StrongAttack : IAttackType
{
   public void Attack() => Console.WriteLine("Using Strong Attack");
}

public class FastAttack : IAttackType
{
   public void Attack() => Console.WriteLine("Using Fast Attack");
}

Here we have two attack types: the strong attack and the fast attack. Each enemy we create will possess one of these attack types for the duration of its existence. Both attack types implement the IAttackType interface, and we provide the appropriate implementation when we create a new enemy object.

In this example, I don’t check for null. Nowadays C# includes the nullable reference types feature, which utilizes static analysis to enable compile-time checking of null values and issues warnings to prevent runtime null reference exceptions.

If you are using an older version of C# that doesn’t support the nullable reference types feature, you should also check for null, as it is one of the biggest pitfalls in constructor injection.

This example demonstrates the classic way to write a class constructor. As of C# 12, we can also utilize the primary constructors feature.

Finally, as you can see, my _attackType field is readonly. Employing a readonly field is preferable, as it limits the number of states our class can have. Because we know that our field is readonly, each object of our class will possess a starting state that is immutable for each readonly field. Limiting the number of states an object has makes our code easier to reason about and debug.

Method Injection

When we supply a dependency as a method parameter, we have method injection. Here’s a modified version of the previous example that uses method injection, along with the constructor injection:

public class Enemy 
{
   private readonly IAttackType _attackType;
   
   public Enemy(IAttackType attackType) => _attackType = attackType;

   public void Attack(IDamageable damageable, int damage) => _attackType.Attack(damageable, damage);
}

public interface IDamageable
{
   void TakeDamage(int damage);
}

public class Player : IDamageable
{
   public void TakeDamage(int damage) => Console.WriteLine($"Got hit for {damage} damage");
}

public interface IAttackType
{
   void Attack(IDamageable damageable, int damage);
}

public class StrongAttack : IAttackType
{
   public void Attack(IDamageable damageable, int damage) => Console.WriteLine($"Using Strong Attack on {damageable}");
}

public class FastAttack : IAttackType
{
   public void Attack(IDamageable damageable, int damage) => Console.WriteLine($"Using Fast Attack on {damageable}");
}

Here we can inject the IDamageable to the Attack method of the Enemy, this will be passed to the Attack method of the IAttackType. None of these classes will know the concrete implementation of the IDamageable. Here we can pass an object of type Player, but later this could be anything that implements the IDamageable interface without touching existing code.

If our method’s dependency varies with each call to the method, then method injection is useful, as the injection is dynamically supplied during the method’s invocation.

Property Injection

Property injection enables us to inject dependencies into writable properties of a class. It is useful when the properties of a class have reasonable defaults, but we want to provide the caller with the ability to supply a different implementation, thereby allowing them to modify the behavior of that class.

Property injection can alter the state of our object when our properties are writable using the set keyword. This can pose a problem, as the more states an object can have, the harder it becomes to debug and understand. Particularly with public properties that can have their values changed from external sources, property injection can make our code difficult to maintain and debug.

Fortunately, as of C# 9, we have the ability to use init only properties. The init keyword allows us to inject the dependency only during the object’s construction, thereby limiting the number of states our object can have.

Here’s the previous example that uses property injection:

public class Enemy 
{
   private readonly IAttackType _attackType = new DefaultAttack();
   
   public IAttackType AttackType
   {
      get => _attackType;
      init => _attackType = value;
   }
}

IAttackType strongAttack = new StrongAttack();
Enemy enemy = new Enemy()
{
   AttackType = strongAttack
};

Here we employ a default value, but if a default value is unavailable, then we should mark our property as required to prevent any null reference exceptions at runtime.

Utilizing the init accessor is preferable because altering an object’s state from an external source in the midst of its lifetime is likely one of the worst methods to introduce bugs and complicate our code’s understanding, debugging, and modification.

A property that can change during an object’s lifetime from external code introduces the problems that DI endeavors to resolve. If any such change is necessary, it is better to encapsulate it within a method of the class.

Where to Inject Dependencies

When utilizing DI, we should ensure that dependencies are created within classes dedicated solely to connecting consumers with their services.

Establishing dependencies between classes scattered throughout our codebase will restrict our ability to make changes to our code without introducing breaking changes.

We should strive to centralize the creation of dependencies in a single area for each of our systems. This approach enables us to pinpoint where code changes should occur when adjustments to behavior are required.

Factories, builders, and IOC containers can assist in achieving this goal. While DI frameworks can simplify this process, they are not always necessary and may sometimes introduce more problems than they solve.

Although there are only three types of dependency injection, determining where to create the concrete implementations that compose the final graph of dependencies in our code is, in reality, the most complex aspect of dependency injection. As a general rule, this should be done as close to the entry point of our application as possible. For example, in a console application, this would be in the Main method.

Wrong Usages of Dependency Injection

Here are some examples of how DI can be misused, resulting in no benefits for our code:

Creation Of Concrete Implementations Inside The Class

Building upon the “Where to Inject Dependencies” paragraph, a common misuse of DI involves supplying the dependency within the class that requires it. For example:

public class Enemy 
{
   private readonly IAttackType _attackType;

   public Enemy(IAttackType attackType) => _attackType = new StrongAttack();
}

Here we have an incorrect usage of DI and abstractions. There is no benefit in using an abstraction like the IAttackType interface here, as it is tightly coupled with a concrete implementation. Any change we wish to make to the behavior of the class would require modifying the class’s code. This defeats the purpose of DI.

Similarly, if we had a factory that provides the IAttackType, the problem remains unchanged. The only difference is that we’ve shifted our dependency from a concrete implementation of the IAttackType to a concrete implementation of our factory.

Even a static factory wouldn’t resolve our issue. We would still have a dependency, but this time it would be on the type itself rather than an object of that type.

In such cases, employing constructor injection and elevating the concrete implementation as close to the entry point as possible is preferable.

Service locator

Another incorrect usage of Dependency Injection is the utilization of the service locator pattern.

The service locator pattern presents two issues:

Firstly, it replaces one dependency with another, the dependency on the service locator itself. Prior to the service locator, our class could not function without the concrete implementation of the dependency. However, now it cannot operate without also including the service locator. Ideally, the class providing the concrete implementations should be dependent on the classes that utilize them, as is the case with an IOC container, rather than the reverse.

Secondly, the service locator introduces a hidden dependency. Anywhere within our class, we can invoke the service locator to provide us with a dependency. This dependency is not immediately apparent to anyone using our code unless they thoroughly inspect our implementation.

In typical cases of DI, any dependency is evident to the users of our code because it is presented as an argument to the constructor or a method. Even in properties, there may be a requirement for initialization during object construction, or they may have a default value. However, with the service locator, the dependency is concealed within our implementation.

Static classes and Singletons

A variation of the above is to move our dependencies from arguments in the constructor to within the class itself. Then, we can obtain the concrete implementations from a static factory or a singleton class. However, this approach presents the same issues as before.

While we create dependencies on these classes, we also conceal the dependencies our class has within its implementation.

Conclusion

Dependency injection is a complex topic. This complexity doesn’t stem from the various types of DI or how and when they should be employed, but rather from the challenge of determining where the concrete implementations of our dependencies should be instantiated and how they should be passed down the dependency chain to their consumers.

The issue isn’t whether someone should use dependency injection or not. A program cannot function without some form of dependency injection. Every programmer has utilized dependency injection, in one form or another, in every object-oriented program they’ve ever developed. The real question lies in where our objects, which we require as dependencies, should be instantiated and how they should be supplied to the classes that require them. This must be done in a manner that ensures our program reaps the six benefits of DI outlined at the beginning of this post: maintainability, debugging, testing, extensibility, parallel programming, and late binding.

Thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.


Follow me: