C# Best Practices: Dependency Injection Done Right

Ashok KAshok K
4 min read

Introduction

In software development, one common problem that developers often face is tight coupling between classes. You might have experienced a situation where making a small change in one class unexpectedly breaks several other classes. This is a classic symptom of tightly coupled code.

Tightly coupled code is hard to maintain, difficult to test, and fragile when it comes to extending functionality. This article explores why tight coupling occurs, why it’s problematic, and how Dependency Injection (DI) — specifically Constructor Injection — can help you write clean, maintainable, and testable C# code.


Understanding Tight Coupling

Tight coupling happens when classes are highly dependent on each other’s concrete implementations. This often occurs when classes create instances of their dependencies internally using the new keyword. For example:

public class Car
{
    private Engine _engine;

    public Car()
    {
        _engine = new Engine(); // Creating dependency inside the class
    }

    public void Start()
    {
        _engine.Run();
    }
}

public class Engine
{
    public void Run()
    {
        Console.WriteLine("Engine started");
    }
}

In this example, the Car class directly creates an instance of Engine. This approach leads to several issues:

  • Difficult to test: You cannot replace Engine with a mock or stub in tests.

  • Hard to maintain: Changes to the Engine class might require changes to Car.

  • Low flexibility: You cannot easily switch to different types of engines without modifying the Car class.


Why is Tight Coupling a Problem?

  1. Reduced Testability: If a class creates its own dependencies, it becomes difficult to write unit tests that isolate that class. You cannot substitute real dependencies with test doubles like mocks or fakes.

  2. Poor Separation of Concerns: Classes that create their own dependencies are responsible for both business logic and managing dependencies, mixing different concerns.

  3. Fragile Code: Changes in one dependency can cause unexpected failures in dependent classes, making the codebase fragile and hard to refactor.

  4. Limited Extensibility: Adding new implementations or swapping dependencies requires modifying existing code, violating the Open/Closed Principle (OCP).


The Solution: Dependency Injection

Dependency Injection is a design pattern that helps solve these problems by inverting the control of dependency creation. Instead of classes creating their own dependencies, dependencies are provided (injected) from outside.

This makes classes focus solely on their responsibilities, while dependency management is handled externally. DI improves testability, maintainability, and flexibility.


Constructor Injection: A Simple and Effective Form of DI

Constructor Injection is one of the most common forms of DI, where dependencies are passed as parameters to the constructor.

This method has several advantages:

  • Explicit dependencies: The required dependencies of a class are clear from its constructor signature.

  • Immutable dependencies: Dependencies can be assigned to readonly fields, preventing accidental reassignment.

  • Simpler testing: You can easily pass mock implementations when writing tests.

  • Better separation: Classes are not responsible for creating dependencies.


Applying Constructor Injection with Interfaces

To maximize flexibility and abstraction, it’s best practice to depend on interfaces instead of concrete classes.

Let’s improve the previous example:

Step 1: Define an interface for the dependency

public interface IEngine
{
    void Run();
}

Step 2: Implement the interface in a concrete class

public class Engine : IEngine
{
    public void Run()
    {
        Console.WriteLine("Engine started");
    }
}

Step 3: Update the dependent class to accept the interface via constructor

public class Car
{
    private readonly IEngine _engine;

    public Car(IEngine engine)
    {
        _engine = engine; // Dependency injected via constructor
    }

    public void Start()
    {
        _engine.Run();
    }
}

Benefits of This Approach

  • You can easily create mock implementations of IEngine for testing.
public class MockEngine : IEngine
{
    public bool WasRunCalled { get; private set; }

    public void Run()
    {
        WasRunCalled = true;
    }
}
  • Your classes follow the Dependency Inversion Principle (DIP) — high-level modules (Car) depend on abstractions (IEngine), not concrete implementations.

  • Swapping out engine implementations in the future requires no changes to the Car class.


Using IoC Containers for Managing Dependencies

In a real-world application, manually creating and injecting dependencies can become tedious as the number of classes grows.

Inversion of Control (IoC) containers, also known as dependency injection frameworks, automate this process by managing object lifetimes and wiring dependencies.

Microsoft’s built-in DI container in ASP.NET Core, for example, allows you to register services like this:

var services = new ServiceCollection();

services.AddTransient<IEngine, Engine>();
services.AddTransient<Car>();

var serviceProvider = services.BuildServiceProvider();

var car = serviceProvider.GetService<Car>();
car.Start();

The container resolves Car, injects an instance of Engine implementing IEngine, and you get fully wired objects without manually new-ing anything.


Summary

Tight coupling in your code causes maintainability, testability, and extensibility issues. By applying Dependency Injection using Constructor Injection and relying on interfaces, you create loosely coupled, flexible, and testable code.

This practice enables:

  • Clear declaration of dependencies

  • Easy unit testing with mock dependencies

  • Better adherence to SOLID principles

  • Easier maintenance and refactoring

Adopting DI early in your projects can save time and headaches in the long run.


Your Experience

Have you encountered tight coupling issues in your projects? How do you implement Dependency Injection in your team? Feel free to share your thoughts or questions in the comments section.

0
Subscribe to my newsletter

Read articles from Ashok K directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ashok K
Ashok K