Dependency Injection in ASP.NET Core: A Complete Developer's Guide From A to Z
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.

Table of Contents
No table of contents available for this article
Introduction
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.
1. Understanding Dependency Injection - Where to Start?
Before diving into DI, we need to understand two prerequisite concepts:
1.1. Dependency Inversion Principle (DIP)
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.
1.2. Inversion of Control (IoC)
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
1.3. Dependency Injection (DI)
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
2. Three Types of Dependency Injection
2.1. Constructor Injection (Most Popular ⭐)
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
2.2. Setter Injection (Property Injection)
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.
2.3. Interface Injection (Less Common)
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.
3. Advantages & Limitations of DI
✅ Advantages
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
❌ Limitations
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
4. Using DI in ASP.NET Core - Hands-On Practice
ASP.NET Core has an incredibly powerful built-in DI container. Let's see how to use it:
Step 1: Define Interface
csharp
public interface IMyDependency
{
void WriteMessage(string message);
}Step 2: Create Implementation
csharp
public class MyDependency : IMyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage: {message}");
}
}Step 3: Register Service in DI Container
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();Step 4: Inject and Use
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
IMyDependencyin the constructorFinds the registered implementation (
MyDependency)Creates an instance and injects it
Advanced Example: Nested Dependencies
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
5. Service Lifetime - The Lifecycle of Services
This is an extremely important part when working with DI. Service Lifetime determines when instances are created and how long they live.
5.1. Transient (Temporary)
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 ID5.2. Scoped (Scoped Lifetime)
csharp
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 ID5.3. Singleton (Single Instance)
csharp
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 requests📊 Service Lifetime Comparison
LifetimeCreated WhenDisposed WhenUse CaseTransientEach injectionRight after useLightweight stateless servicesScopedEach requestRequest endsDbContext, unit of workSingletonApp startApp shutdownConfiguration, cache, logging6. Best Practices & Tips
✅ Do's
Always 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'ts
Don't inject Scoped service into Singleton → Captive Dependency
Don't create instances manually with
newkeyword when DI is availableDon't inject too many dependencies → Code smell (>5 dependencies)
Don't dispose services manually - let the DI container handle it
🔍 Common Pitfall: Captive Dependency
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 requestConclusion
Dependency 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.csInject through constructor
Start applying DI in your next project - you'll see the difference immediately!
References
[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