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
):
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();
Step 1: Setting Up the Project
- Create an ASP.NET Core MVC Web Application using Visual Studio.
- 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!