Learn everything about Dependency Injection in ASP.NET Core - from fundamental concepts to practical implementation. Clear explanations of DI, IoC, and Service Lifetime with easy-to-understand code examples for all levels.

No table of contents available for this article
Dependency Injection (DI) is one of the most crucial concepts that every .NET Core developer needs to master. If you've ever wondered why some developers' code is so maintainable, testable, and flexible - the answer lies in DI.
In this article, we'll explore DI together, from foundational concepts to practical implementation in ASP.NET Core.
Before diving into DI, we need to understand two prerequisite concepts:
This is the "D" in SOLID principles - one of the foundations of object-oriented programming.
Core Principle:
High-level modules should not depend directly on low-level modules
Both should depend on abstractions (interfaces/abstract classes)
Interfaces should not depend on implementation details, but vice versa
Real-World Example:
Imagine you're building a customer management system:
csharp
// ❌ WRONG APPROACH - Violates DIP
public class CustomerService
{
private SqlServerDatabase _database;
public CustomerService()
{
_database = new SqlServerDatabase(); // Direct dependency!
}
public void SaveCustomer(Customer customer)
{
_database.Save(customer);
}
}Problem: If tomorrow you want to switch from SQL Server to MongoDB, you'll have to modify the entire CustomerService class. One small change triggers a cascade of modifications.
csharp
// ✅ CORRECT APPROACH - Follows DIP
public interface IDatabase
{
void Save(Customer customer);
}
public class SqlServerDatabase : IDatabase
{
public void Save(Customer customer)
{
// SQL Server save logic
}
}
public class MongoDatabase : IDatabase
{
public void Save(Customer customer)
{
// MongoDB save logic
}
}
public class CustomerService
{
private readonly IDatabase _database;
public CustomerService(IDatabase database)
{
_database = database; // Depends on abstraction
}
public void SaveCustomer(Customer customer)
{
_database.Save(customer);
}
}Benefit: Now you can easily switch databases without touching CustomerService.
IoC is a design pattern that helps code follow the DIP principle. Instead of classes creating their own dependencies, they receive them from outside.
Multiple ways to implement IoC:
Service Locator
Events
Delegates
Dependency Injection ← The most popular approach
DI is a specific technique to implement the IoC Pattern.
Operating Principles:
Communication through interfaces: Modules don't call each other directly, but through interfaces
External injection: Dependencies are "injected" into a class from outside, not self-created
DI Container manages: A "container" (like ASP.NET Core's built-in DI) handles creating and providing dependencies
Flexible configuration: You configure the mapping between interfaces and implementations in code
Why do we need DI?
✅ Reduces coupling between modules
✅ Code is easier to maintain and extend
✅ Testing becomes extremely simple
✅ Easy to swap implementations
Dependencies are passed through the constructor:
csharp
public class OrderService
{
private readonly IPaymentGateway _paymentGateway;
private readonly IEmailService _emailService;
// Dependencies injected through constructor
public OrderService(IPaymentGateway paymentGateway, IEmailService emailService)
{
_paymentGateway = paymentGateway;
_emailService = emailService;
}
public void ProcessOrder(Order order)
{
_paymentGateway.Process(order.Total);
_emailService.SendConfirmation(order.Email);
}
}Advantages:
Dependencies are clear from initialization
Immutable - safe for multi-threading
Easy to unit test
Dependencies are passed through property setters:
csharp
public class ReportGenerator
{
public ILogger Logger { get; set; }
public void Generate()
{
Logger?.LogInfo("Generating report...");
}
}When to use: When the dependency is optional.
Dependency provides an interface with an inject method:
csharp
public interface IDependencyInjector
{
void InjectDependency(IDependency dependency);
}In Practice: This type is rarely used in .NET Core.
1. Reduced Coupling (Loose Coupling)
Modules are independent, not tightly coupled
Changing one module doesn't affect others
2. Easy Maintenance
Clear, readable code
Quick bug fixes
Safer refactoring
3. Simple Testing
csharp
// Easy to mock dependencies in unit tests
var mockPayment = new Mock<IPaymentGateway>();
mockPayment.Setup(x => x.Process(It.IsAny<decimal>())).Returns(true);
var orderService = new OrderService(mockPayment.Object, emailService);4. High Flexibility
Easy implementation swapping
Support for multiple implementations
1. Learning Curve
Initially difficult concept for beginners
Takes time to get familiar with patterns
2. More Complex Debugging
Don't know exactly which implementation is injected
Need tools to trace dependencies
3. Performance Overhead
Initializes many objects at app start
Reflection can slow things down (but minimal impact)
4. Increased Complexity
Code has multiple abstraction layers
Requires good project structure
ASP.NET Core has an incredibly powerful built-in DI container. Let's see how to use it:
csharp
public interface IMyDependency
{
void WriteMessage(string message);
}csharp
public class MyDependency : IMyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage: {message}");
}
}In Program.cs (or Startup.cs for older .NET Core):
csharp
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency>();
var app = builder.Build();csharp
public class Index2Model : PageModel
{
private readonly IMyDependency _myDependency;
// Constructor Injection
public Index2Model(IMyDependency myDependency)
{
_myDependency = myDependency;
}
public void OnGet()
{
_myDependency.WriteMessage("Index2Model.OnGet called");
}
}The Magic: ASP.NET Core automatically:
Detects IMyDependency in the constructor
Finds the registered implementation (MyDependency)
Creates an instance and injects it
csharp
public class MyDependency2 : IMyDependency
{
private readonly ILogger<MyDependency2> _logger;
// MyDependency2 also has a dependency on ILogger
public MyDependency2(ILogger<MyDependency2> logger)
{
_logger = logger;
}
public void WriteMessage(string message)
{
_logger.LogInformation($"MyDependency2.WriteMessage: {message}");
}
}DI Container automatically resolves the entire dependency chain: PageModel → IMyDependency → ILogger
This is an extremely important part when working with DI. Service Lifetime determines when instances are created and how long they live.
csharp
builder.Services.AddTransient<IMyService, MyService>();Characteristics:
Each request for the service → creates a new instance
Different dependencies in the same request may receive different instances
When to use:
Lightweight, stateless services
Services that only process and return results
Example:
csharp
public class TransientService : ITransientService
{
private readonly Guid _id;
public TransientService()
{
_id = Guid.NewGuid();
Console.WriteLine($"TransientService created: {_id}");
}
}
// Each injection will log a different IDcsharp
builder.Services.AddScoped<IMyService, MyService>();Characteristics:
Each HTTP request → creates 1 unique instance
Within the same request, everywhere uses the same instance
When request ends → instance is disposed
When to use:
Database contexts (Entity Framework)
Services that need to share state within a request
This is the most common lifetime in web apps
Example:
csharp
public class ScopedService : IScopedService
{
private readonly Guid _id = Guid.NewGuid();
public void ShowId()
{
Console.WriteLine($"ScopedService ID: {_id}");
}
}
// Within the same request, ShowId() will always print the same IDcsharp
builder.Services.AddSingleton<IMyService, MyService>();Characteristics:
Creates 1 unique instance when the app starts
This instance is shared across the entire application
Exists until app shutdown
When to use:
Configuration services
Caching services
Stateless or thread-safe services
⚠️ Note:
Be careful with memory leaks
Must be thread-safe
Should not inject Scoped/Transient services into Singleton
Example:
csharp
public class CacheService : ICacheService
{
private readonly Dictionary<string, object> _cache = new();
public void Set(string key, object value)
{
_cache[key] = value;
}
public object Get(string key)
{
return _cache.GetValueOrDefault(key);
}
}
// _cache will exist and be shared across all requestsLifetimeCreated WhenDisposed WhenUse CaseTransientEach injectionRight after useLightweight stateless servicesScopedEach requestRequest endsDbContext, unit of workSingletonApp startApp shutdownConfiguration, cache, loggingAlways inject through constructor (top priority)
Register interfaces, not concrete classes
Use Scoped for DbContext
Keep constructors simple - only assign dependencies
Avoid service locator pattern - always use injection
Don't inject Scoped service into Singleton → Captive Dependency
Don't create instances manually with new keyword when DI is available
Don't inject too many dependencies → Code smell (>5 dependencies)
Don't dispose services manually - let the DI container handle it
csharp
// ❌ WRONG - Singleton contains Scoped dependency
builder.Services.AddSingleton<IMySingleton, MySingleton>();
builder.Services.AddScoped<IMyScoped, MyScoped>();
public class MySingleton : IMySingleton
{
private readonly IMyScoped _scoped; // Dangerous!
public MySingleton(IMyScoped scoped)
{
_scoped = scoped; // Scoped service is "captured" by Singleton
}
}
// Result: IMyScoped will live as long as Singleton, not disposed after requestDependency Injection is not just a pattern, but a design mindset that makes your code:
Flexible - Easy to change and extend
Testable - Simple dependency mocking
Maintainable - Clear code, fewer bugs
Professional - Follows SOLID principles
ASP.NET Core has a powerful built-in DI container, you just need to:
Define interface
Create implementation
Register in Program.cs
Inject through constructor
Start applying DI in your next project - you'll see the difference immediately!
[1] Viblo - Understanding Dependency Injection in 6 Minutes
[2] TopDev - Introduction to Inversion of Control and Dependency Injection
[3] Microsoft Docs - Dependency Injection in ASP.NET Core
[4] Microsoft Docs - .NET Dependency Injection Best Practices
[5] Martin Fowler - Inversion of Control Containers and the Dependency Injection Pattern