Back to All Posts

Dependency Injection (DI) or no Dependency Injection in the .NET Framework

In .NET development, we often hear the terms Inversion of Control or Dependency Injection used interchangeably, which is normal considering they refer to the same thing.

Inversion of Control is a programming style where a framework or runtime controls the program flow. It was first brought into discussion in Designing Reusable Classes, published by the Journal of Object-Oriented Programming in 1988.  It’s like saying that your car is special because it has wheels.

Later, Inversion of control over Dependencies became a standalone term, named – Dependency Injection, and it refers to dependency management as a subset of Inversion of Control. The idea behind it is to have a mechanism that provides proper implementation over an abstraction.

So, why worry about dependencies and about knowing the types of dependencies that your code can have? The answer is simple; because you want to know how to handle them accordingly, to make your code easy to change and to adapt to new requirements without spending a lot of time and unnecessary effort implementing them. Let’s face it, have you ever seen a project that has no change requests? Ever?

In C#, every time you use the new keyword, you introduce a dependency in your code. Surprisingly, not all dependencies you introduce are bad for you and your code, but you need to know them to separate the good from the ugly.

For example, if you are using and instantiating StringBuilder, you need to ask yourself if this type is ever prone to change. No, it is not, because it is unlikely that you will ever supply your own implementation of StringBuilder and swap what .NET as a platform provides for you (that is one of the reasons why it is called a framework, and not a library of some kind).

 

Types of Dependencies

Hidden Dependencies

At a first glance, you might think that a class has no dependency, but it simply instantiates what is needed on its constructor, directly creating what it uses. And in this case, you might think that the CustomerAccount is very easy to use and testable since it has an empty constructor. But is it?

public class CustomerAccount
{
  private IBankAccount savingsAccount;
  private IBankAccount currentAccount;
  public CustomerAccount()
  {
    this.savingsAccount = new SavingsAccount();
    this.currentAccount = new CurrentAccount();
  }
}

 

Visible/Direct Dependencies

The visible/direct dependencies are very easy to spot:

 public CustomerAccount(IBankAccount currentAccount)
{
  this.currentAccount = currentAccount;
}

 

Volatile Dependencies

The volatile dependencies have the following characteristics:

  • They are needed whenever you use a third party that involves a license and needs to be installed on multiple machines or servers,
  • The dependency introduces the requirement to configure an environment,
  • The implementation hasn’t been yet created, hence the inability to develop modules in parallel,
  • The dependency contains non-deterministic behavior. You can’t rely on DateTime.Now or test a random number because all tests should and need to be deterministic.

If you are encountering an inability to test or you might lack late binding or extensibility, you might deal with a volatile dependency.

 

Stable Dependencies

Stable dependencies can be represented by modules that are already implemented and have new versions that won’t cause compilation errors or breaking changes in your application.

These are unlikely to change, and more often already implemented in frameworks, for you to just use them as they come. The DateTime type should not be changed with your own implementation (I am not talking about overriding things).

 

Loose Coupling Dependencies

Let’s take the following example:

 public class BankAccount
{
  private CurrentAccount currentAccount;
  private SavingsAccount savingsAccount;
  public BankAccount(CurrentAccount currentAccount, SavingsAccount savingsAccount)
  {
    this.currentAccount = currentAccount;
    this.savingsAccount = savingsAccount;
  }
}

Is there something wrong with the example? Well, yes, because whenever you want to use the BankAccount type you need to provide 2 objects already instantiated. Nothing unusual so far, but what happens if you need SavingsAccount and CurrentAccount to be instantiated, validated and so on? Or if you need to change or add another type of account? Let’s say, CreditAccount?

At this point, you will have a lot of work to do. Not only you will have to implement CreditAccount, but you will need additional work in the BankAccount class to support CreditAccount, and you might break classes that already use the current implementation.

By using an abstraction and removing the strongly typed dependencies in your code, you improve testability and at the same time make your code loosely coupled and more maintainable.

So, let’s rewrite the class:

public class BankAccount
{
  private IBankAccount currentAccount;
  private IBankAccount savingsAccount;
  public BankAccount(IBankAccount currentAccount, IBankAccount savingsAccount)
  {
    this.currentAccount = currentAccount;
    this.savingsAccount = savingsAccount;
  }
}

As a result, whenever you code against abstractions you have loosely coupled dependencies that are easy to swap, test and maintain.

 

Tightly Coupled Dependencies

 public class BankAccount
{
  private SavingsAccount savingsAccount = new SavingsAccount();
  private CurrentAccount currentAccount = new CurrentAccount();
  public decimal GetTotalForAccount(Guid accountNumber)
  {
    decimal currentAccountMoney = this.currentAccount.GetMoneyByAccountNumber(accountNumber);
    decimal savingsAccountMoney = this.savingsAccount.GetMoneyByAccountNumber(accountNumber);
    return currentAccountMoney + savingsAccountMoney;
  }
}

In this case, we have no abstraction to deal with. Even more, the class looks like it uses the default constructor. Writing “fast and furious” code, without any abstractions is the first instinct of junior developers, and is somehow acceptable; the code does what it should do and it works. But it works up until the point some change requests are issued or there is a sudden need for writing unit tests. From that moment on, adding or replacing a component becomes a time-consuming, error prone work.

 

Dependency Injection Anti-patterns

Control Freak

Control Freak is the most common DI anti-pattern and the default way of creating instances.

Even if we weren’t aware, we were “Control Freaks” at some point. We all created instances of objects in a class just to use a method or two and never asked ourselves if the Single Responsibility principle is applied or not. Basically, the Single Responsibility Principle states that a class should do one thing and one thing only and that should be entirely encapsulated in the class.

When you start using the new keyword, you directly or indirectly introduce dependencies. That is when you should take into consideration that if a class prepares the stage for itself and creates objects to consume instead of receiving them, it breaks SRP and in the long run it brings you extra costs on maintainability.

You should think of the creation of objects that are consumed by a class as a separate job, because it does not help you obtain high cohesion. Even if it might look like it helps, the ‘cohesion’ you think you obtain does not reduce complexity or increase reusability and maintainability.

Let’s take an example:

 public class ProductService
  {
    private readonly ProductRepository repository;
    public ProductService()
    {
      Console.WriteLine("Hi, I'm ProductService and I'll instantiate a repository ");
      //OMG new keyword
      this.repository = new ProductRepository();
      Console.WriteLine("OMG, I just used new keyword ");
      Console.ReadKey();
    }
}

In this case, ProductService should not create its own ProductRepository and should receive it as an injection in constructor to follow the Single Responsibility principle and keep matters separated.

In this case, you should rewrite the ProductService class towards DI, even if you do not intend to use a DI Container.

 public class ProductService
  {
    private readonly ProductRepository repository;
    public ProductService(ProductRepository repository)
    {
      if (repository == null)
      {
        throw new ArgumentNullException("repository");
      }
      this.repository = repository;
    }
  }

In the Control Freak anti-pattern we cannot change implementations or develop in parallel, and can have problems with coupling.

 

Conclusion

There are two types of developers speaking from the DI usage point of view. Those who use it because it is cool or that they really see the benefit of it, and those who do not use it because they think they will pay with performance for it or believe that it is useless.

And really, why work towards something that in the end will generate an entire hierarchy of classes for every request and you cannot control what’s happening behind the scenes?  Yes, I know you want and need to be in charge of what is instantiated and when, and of what is used and where. So, why give up this degree of control because someone invented a library that can wire up a few things for you and also makes you work with interfaces and write everything as is needed by that library? All this hassle just to allow a little testability?

Well, no, because Dependency Injection is not all about testability. It is also about separating concerns, following SOLID principles, maintainability, and if I can add… about making your day to day life as a developer easier.

About the Author

Senior Software Developer

Irina

Software architect, Microsoft Certified Trainer and MCSD for Web and Application Lifecycle Management. Above all, passionate about leadership and personal development, as well as creating learning contexts for her team.

Your email address will not be published. Required fields are marked *