Dependency Management in ASP.NET Core

AZHARUL | 03-Dec-2024 08:52:50 AM | Time to read: 9 Min

Hello, dear readers and programming enthusiasts! Dependency Management is a cornerstone of building maintainable, testable, and scalable applications in ASP.NET Core. By managing dependencies properly, we can write loosely coupled code, making our application easier to extend, test, and maintain.

In this blog, we’ll explore Dependency Management in ASP.NET Core step-by-step with practical, professional examples. We’ll address common developer challenges and demonstrate how Dependency Injection (DI) can solve them.

 

What is Dependency Management?

Dependency Management is the process of handling dependencies (objects that a class requires to function) in a way that minimizes tight coupling and maximizes flexibility.

For example, imagine a ProductController that relies on a ProductService to fetch product data. If the controller directly creates an instance of the service (new ProductService()), it becomes tightly coupled, making the code rigid and harder to maintain. However, With DI, the controller depends only on an abstraction (e.g., IProductService), not a specific implementation. We don't need to modify the controller code. Instead, we configure the DI container in one place (e.g., Program.cs):

 

Why Dependency Management Matters?

·  Loose Coupling
Classes depend on abstractions (interfaces) rather than concrete implementations, making them easier to swap or extend.

·  Testability
Dependencies can be mocked for unit testing without changing production code.

·  Maintainability
Centralized registration of dependencies simplifies application management and reduces duplicate code.

·  Flexibility
Easily replace or update implementations without affecting other parts of the application.

 

Key Concepts in Dependency Management

 
1. Dependency Injection (DI)

Dependency Injection (DI) is the pattern used in ASP.NET Core for dependency management. DI works by:

· Registering Services: Services (dependencies) are registered in the DI container during application startup.

· Resolving Services: ASP.NET Core automatically resolves these services and injects them where needed.

 
2. Service Lifetimes

When registering services, we specify their lifetime:

  • Singleton: A single instance is created and shared throughout the application.
    Use when the service doesn’t maintain state and can be shared.
builder.Services.AddSingleton<IProductService, ProductService>();
  • Scoped: A new instance is created per request (e.g., per HTTP request).
    Ideal for database contexts or services tied to request-specific data.
builder.Services.AddScoped<IProductService, ProductService>();
  • Transient: A new instance is created every time the service is requested.
    Suitable for lightweight, stateless services.
builder.Services.AddTransient<IProductService, ProductService>();

 

3. Registering Services in ASP.NET Core

Services are registered in the Program.cs file.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IProductService, ProductService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddTransient<IEmailService, EmailService>();
var app = builder.Build();
app.Run();

 

Dependency Management in Action

Let’s build an example in ASP.NET Core MVC using Dependency Injection to manage dependencies.

Step 1: Setting Up the Project
  1. Create an ASP.NET Core MVC Web Application using Visual Studio.
  2. Add the following class libraries:
    • BusinessLogicLibrary (for business logic like services)
    • DataAccessLibrary (for data access logic)
 
Step 2: Implementing the Layers
  • Data Access Layer

In DataAccessLibrary, create a Product model, IProductRepository interface and ProductRepository:

namespace DataAccessLibrary.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Category { get; set; }
        public double Price { get; set; }
    }
}

namespace DataAccessLibrary.Repositories
{
    public interface IProductRepository
    {
        List<Product> GetProductsByCategory(string category);
    }

    public class ProductRepository : IProductRepository
    {
    	private List<Product> _products = new()
    	{
        	new Product { Id = 1, Name = "Smartphone", Category = "Electronics", Price = 800 },
        	new Product { Id = 2, Name = "Tablet", Category = "Electronics", Price = 1200 }
    	};
    	public List<Product> GetProductsByCategory(string category)
    	{
        	return _products.Where(p => p.Category == category).ToList();
    	}
	}
}
 
  • Business Logic Layer

In BusinessLogicLibrary, create a service and interface:

public interface IProductService
{
    List<Product> GetProductsByCategory(string category);
}
public class ProductService : IProductService
{
    private readonly ProductRepository _productRepository;
    public ProductService(ProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    public List<Product> GetProductsByCategory(string category)
    {
        return _productRepository.GetProductsByCategory(category);
    }
}
 
Step 3: Registering Dependencies

In Program.cs, register the services:

builder.Services.AddScoped<ProductRepository>();
builder.Services.AddScoped<IProductService, ProductService>();
 
Step 4: Using DI in Controllers

Inject the IProductService into the ProductController:

public class ProductController : Controller
{
    private readonly IProductService _productService;
    public ProductController(IProductService productService)
    {
        _productService = productService;
    }
    public IActionResult Electronics()
    {
        var products = _productService.GetProductsByCategory("Electronics");
        return View(products);
    }
}

 

Advantages of Dependency Injection(DI)

 
1. Easy Swapping of Implementations

Imagine we need to use ProductService during development and ApiProductService in production.

The ProductService returns a list of products from our local memory or database, while ApiProductService gets the product list from an external API. Now, let's say we want to switch to using the API-based ApiProductService instead.

  • Create a new ApiProductService class:
public class ApiProductService : IProductService
{
    public List<Product> GetProductsByCategory(string category)
    {
        // Simulated API call
        return new List<Product>
        {
            new Product { Id = 1, Name = "Smartphone", Category = "Electronics", Price = 800 }
        };
    }
}
  • Update Program.cs:
builder.Services.AddScoped<IProductService, ApiProductService>();

 

2. Simplified Testing

Mock the IProductService during unit testing:

var mockService = new Mock<IProductService>();
mockService.Setup(s => s.GetProductsByCategory("Electronics"))
    .Returns(new List<Product> { new Product { Id = 1, Name = "MockProduct", Category = "Electronics", Price = 100 } });
var controller = new ProductController(mockService.Object);

 

  • Reduced Code Duplication

All dependency-related logic is centralized in Program.cs.

 


 

Scenarios Highlighting the Challenges of Working Without Dependency Management

When we directly instantiate a service (ProductService) in our controller, we tightly couple our controller to that service. This creates several issues:

 
Scenario 1: Changing or Swapping to a New Implementation

Let’s say we want to replace ProductService with a new implementation ApiProductService , and this  ApiProductService is  fetching products from an API.

 

Without Dependency Injection (Direct Instantiation or Tightly Coupled)
If we decide to use ApiProductService instead of ProductService, we must modify every controller (or other classes) that directly instantiates ProductService

Example:

public class ProductController
{
    private readonly ProductService _productService;
    public ProductController()
    {
          //----------Change required here----------
        _productService = new ProductService();
    }
}

 

With Dependency Injection(Loosely Coupled)
With DI, the controller depends only on an abstraction (IProductService), not a specific implementation. We don't need to modify the controller code. Instead, we configure the DI container in one place (Program.cs):

builder.Services.AddSingleton<IProductService, ApiProductService>();

The DI container automatically provides ApiProductService to the controller without changing its code.

 
Scenario 2: Adding New Implementations Based on Conditions (e.g. Environment-Specific Services)

Suppose we want ProductService in development and ApiProductService in production.

 

Without Dependency Injection(Tightly Coupled)
We must handle such logic in every controller, making the code repetitive and prone to errors:

public class ProductController
{
    private readonly ProductService _productService;
    public ProductController()
    {
        // we have to Add logic in every controller
        #if DEBUG
            _productService = new ProductService();
        #else
            _productService = new ApiProductService();
        #endif
    }
}

 

With Dependency Injection(Loosely Coupled)
With DI, we configure this logic centrally(Program.cs), and the controllers remain unaffected:

#if DEBUG
    builder.Services.AddSingleton<IProductService, ProductService>();
#else
    builder.Services.AddSingleton<IProductService, ApiProductService>();
#endif

 

Key Problems Without Dependency Injection(DI):

 

Manual Changes Across the Codebase:

  • Suppose ProductService is called in 10 different controllers or services. We will have to update all 10 instances to match the new method signature.
  • This introduces a high chance of human error, where we might forget or overlook an occurrence.

 

Rigid Implementation:

  • If ProductService's implementation changes, we must manually replace or adjust it wherever it's directly instantiated. This breaks the Open/Closed Principle (a fundamental design principle that states a class should be open for extension but closed for modification).

 

Testing Challenges:

  • Without DI, replacing ProductService with a mock or alternative implementation during unit testing becomes cumbersome. We’d have to modify our controller or service logic to accommodate the mock.

 

Dependency Management in ASP.NET Core is a game-changer for building scalable, testable, and maintainable applications. By using DI, we can write loosely coupled code that is easier to manage and extend in real-world software development.

 

If you found this blog helpful, please share it with your peers and colleagues. Don't forget to subscribe to our YouTube channel(Visit Codexoom on YouTube) for more tutorials on ASP.NET Core and professional software development tips!

Thank you for joining us, and as always, happy coding! Until next time, stay curious and keep building amazing things. Take care!