Dependency Inversion Principle VS Dependency Injection

Understanding the difference between the Dependency inversion principle and the Dependency injection using ASP.NET Core

Yohan Malshika
5 min readJan 3, 2024
Dependency Inversion Principle VS Dependency Injection

In the realm of object-oriented programming, the principles of Dependency Inversion (DI) and Dependency Injection (DI) play pivotal roles in achieving maintainable, scalable, and loosely coupled code. In C#, these principles are fundamental for writing robust and testable applications. Let’s delve into these concepts, understand their significance, and explore practical examples in C#.

Dependency Inversion Principle (DIP)

Dependency Inversion is a principle proposed by Robert C. Martin (Uncle Bob) as part of the SOLID principles. It suggests that high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

In simpler terms, this principle emphasizes:

  • Abstraction over concrete implementations: It promotes defining interfaces or abstract classes to decouple high-level modules from low-level implementation details.
  • Inverting the direction of dependency: Instead of classes depending directly on other concrete classes, they depend on interfaces or abstractions.

Consider a scenario where a BusinessLogic class directly depends on a DataAccess class:

public class BusinessLogic
{
private readonly DataAccess _dataAccess;

public BusinessLogic()
{
_dataAccess = new DataAccess();
}

// Methods using _dataAccess
}

This tight coupling makes the BusinessLogic class reliant on the DataAccess class, making it difficult to substitute or test different implementations of the DataAccess.

By adhering to the Dependency Inversion Principle, we can refactor this code:

public interface IDataAccess
{
// Method signatures
}

public class DataAccess : IDataAccess
{
// Implement methods
}

public class BusinessLogic
{
private readonly IDataAccess _dataAccess;

public BusinessLogic(IDataAccess dataAccess)
{
_dataAccess = dataAccess;
}

// Methods using _dataAccess
}

Now, BusinessLogic relies on the IDataAccess interface rather than a specific implementation (DataAccess). This abstraction allows for flexibility, enabling different implementations of IDataAccess to be injected, facilitating easier testing, and switching between data access strategies.

Dependency Injection (DI)

Dependency Injection is a design pattern that implements the Dependency Inversion Principle. It’s a technique where objects receive their dependencies from an external source rather than creating them internally. This external source can be provided through constructors, properties, or methods.

In C#, dependency injection can be achieved in various ways:

  1. Constructor injection
  2. Property injection
  3. Method injection

Let’s check simple examples for each type.

Constructor Injection

Let’s check a simple code example for constructor injection:

public class BusinessLogic
{
private readonly IDataAccess _dataAccess;

public BusinessLogic(IDataAccess dataAccess)
{
_dataAccess = dataAccess;
}

// Methods using _dataAccess
}

Here, the dependency (IDataAccess) is injected through the constructor.

Property Injection

Let’s check a simple code example for property injection:

public class BusinessLogic
{
public IDataAccess DataAccess { get; set; }

// Other methods using DataAccess
}

In this case, the dependency is injected via a public property, which might lead to optional dependencies.

Method Injection

Let’s check a simple code example for method injection:

public class BusinessLogic
{
public void PerformOperation(IDataAccess dataAccess)
{
// Use dataAccess for operation
}
}

The dependency is provided as a parameter to the method, which is suitable for short-lived dependencies or one-off operations.

Dependency Injection in ASP.NET Core

Frameworks like ASP.NET Core provide built-in support for dependency injection, allowing you to register services and inject dependencies automatically through constructor injection.

You can register your services like the below code in program.cs. Mostly, we register DI services in program.cs.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IDataAccess, DataAccess>();
// Other service registrations

Here, we have registered services using a transient lifetime for DI. After that, you can inject your service through constructor injection.

public class BusinessLogic
{
private readonly IDataAccess _dataAccess;

public BusinessLogic(IDataAccess dataAccess)
{
_dataAccess = dataAccess;
}

// Methods using _dataAccess
}

The framework(ASP.NET Core) resolves and injects the specified dependencies when creating instances of classes.

Understanding Dependency Lifetimes in ASP.NET Core

In the context of Dependency Injection (DI) in C#, the lifetime or scope of a dependency managed by a DI container typically refers to how long an instance of a particular dependency is kept alive. There are three common lifetimes or scopes that are often used:

  1. Transient: Transient lifetime means that a new instance of the dependency is created every time it is requested or injected into a consuming class. This scope is suitable for stateless services or components that do not hold state across multiple requests. With transient lifetime, a new instance is created on every request for that particular dependency.
builder.Services.AddTransient<IDataAccess, DataAccess>();

2. Singleton: Singleton lifetime means that the container maintains a single instance of the dependency throughout the application’s lifetime. This instance is reused whenever the dependency is requested, ensuring that all parts of the application share the same instance.

builder.Services.AddSingleton<IDataAccess, DataAccess>();

3. Scoped: Scoped lifetime means that a single instance of the dependency is created once per scope or per request. In the case of web applications, this scope often aligns with an HTTP request. Within the same HTTP request, the same instance is reused, but subsequent requests receive their own instances.

builder.Services.AddScoped<IDataAccess, DataAccess>();

These different lifetimes cater to various scenarios in application development. Developers should choose the appropriate lifetime based on the specific requirements and behavior needed for the components within their applications. Understanding these lifetimes helps in managing memory efficiently and ensuring the expected behavior of dependencies throughout the application’s lifecycle.

Benefits of Dependency Injection

  1. Decoupling: DI facilitates loose coupling by separating the creation of dependencies from the dependent classes, enhancing flexibility and maintainability.
  2. Testability: By injecting dependencies, it becomes easier to mock or substitute dependencies during unit testing, allowing for isolated and efficient testing of individual components.
  3. Scalability and Reusability: DI promotes the reuse of components by allowing different implementations to be injected, facilitating scalability and adaptability in the application architecture.

Conclusion

Dependency Inversion and Dependency Injection are crucial concepts in modern software development, especially in C# and other object-oriented languages. These principles promote maintainable, testable, and scalable code by decoupling dependencies and enabling flexibility in the implementation of components.

By employing these principles, developers can create modular, reusable, and easily maintainable software systems. Mastering Dependency Injection empowers developers to write cleaner, more maintainable codebases that are easier to extend and test, ultimately leading to more robust and flexible applications.

--

--