This article is designed to remove the mystery.
By the end, you’ll clearly understand:
- What dependency injection actually is (without buzzwords)
- • Why DI exists and what problems it solves
- • How .NET’s DI container works internally
- • Service lifetimes: Singleton, Scoped, and Transient
- • How memory, scopes, and disposal work
- • How middleware interacts with DI
- • How logging fits into DI and request scopes
- • Real-world examples you’ll recognize from production apps
Let’s start from the beginning.
------------------------------------------------------------
WHAT DEPENDENCY INJECTION REALLY IS
------------------------------------------------------------
At its core, dependency injection answers a simple question: “How does a class get the things it needs to do its job?” Without DI, classes usually create their own dependencies:
public class WeatherController
{
private readonly WeatherService _service = new WeatherService();
}
This approach tightly couples your code. The controller now:
- • Knows the concrete implementation
- • Can’t easily be tested
- • Can’t swap implementations without rewriting code
With DI, the class simply declares what it needs:
public class WeatherController
{
private readonly IWeatherService _service;
public WeatherController(IWeatherService service)
{
_service = service;
}
}
Now:
- • The controller depends on an abstraction
- • Another part of the system decides what implementation to use
- • Testing becomes trivial
- • Lifetimes can be managed centrally
Dependency Injection is not a framework feature—it’s a design pattern. ASP.NET Core simply provides a built-in container to support it.
------------------------------------------------------------
WHY DEPENDENCY INJECTION EXISTS
------------------------------------------------------------
DI solves several real problems:
- 1. Loose coupling
- Your code depends on interfaces, not concrete classes.
- 2. Testability
- You can inject mocks or fakes without rewriting logic.
- 3. Lifetime management
- You control how long objects live and when they’re disposed.
- 4. Centralized configuration
- All object creation rules live in one place.
------------------------------------------------------------
THE .NET DI PIPELINE (THE BIG PICTURE)
------------------------------------------------------------
In ASP.NET Core, DI works in four stages:
1. Service registration (IServiceCollection)
2. Container creation (IServiceProvider)
3. Request scope creation (per HTTP request)
4. Dependency resolution (when needed)
Important:
The container does NOT create all services at startup.
It creates services only when something asks for them.
------------------------------------------------------------
A SIMPLE DI EXAMPLE
------------------------------------------------------------
public interface IClock
{
DateTime Now { get; }
}
public class SystemClock : IClock
{
public DateTime Now => DateTime.Now;
}
Register it:
builder.Services.AddScoped<IClock, SystemClock>();
Inject it into an endpoint:
app.MapGet("/time", (IClock clock) => new { clock.Now });
The DI container sees the request, creates a scope, resolves IClock, and injects it.
------------------------------------------------------------
SERVICE LIFETIMES (THIS IS THE MOST IMPORTANT PART)
------------------------------------------------------------
TRANSIENT
builder.Services.AddTransient<IClock, SystemClock>();
Meaning:
• A new instance is created every time it’s requested
Use cases:
• Lightweight helpers
• Stateless utilities
SCOPED
builder.Services.AddScoped<IClock, SystemClock>();
Meaning:
• One instance per HTTP request
• Shared across all components in that request
Use cases:
• DbContext
• Request context
• Per-request caching
SINGLETON
builder.Services.AddSingleton<IClock, SystemClock>();
Meaning:
• One instance for the entire lifetime of the application
Use cases:
• Configuration
• Caching engines
• Stateless, thread-safe services
------------------------------------------------------------
THE RULE OF LIFETIMES
------------------------------------------------------------
You can safely depend in this direction:
Singleton → Singleton
Scoped → Scoped or Singleton
Transient → Anything
You should NOT do this:
Singleton → Scoped
That causes runtime failures or subtle bugs.
------------------------------------------------------------
WHAT’S HAPPENING IN MEMORY
------------------------------------------------------------
Stack:
• Method calls
• Local variables (references)
Heap:
• Objects created by DI
Scoped services:
• Stored in the request scope
• Released at end of request
Singletons:
• Stored in the root container
• Live for the lifetime of the app
This is why singletons holding request data cause memory leaks.
------------------------------------------------------------
DISPOSAL (WHY SCOPED MATTERS)
------------------------------------------------------------
If a service implements IDisposable:
• Scoped services are disposed at the end of the request
• Singleton services are disposed when the app shuts down
• Transient services are disposed when their owning scope ends
This is why DbContext must be scoped.
------------------------------------------------------------
MIDDLEWARE + DI (COMMON CONFUSION)
------------------------------------------------------------
Middleware is often long-lived.
That’s why injecting scoped services into middleware constructors causes errors.
Correct approach:
Inject scoped services via InvokeAsync parameters.
public async Task InvokeAsync(HttpContext context, IRequestContext ctx)
{
await _next(context);
}
ASP.NET resolves scoped services from HttpContext.RequestServices.
------------------------------------------------------------
LOGGING WITH DI
------------------------------------------------------------
ILogger<T> is resolved via DI automatically.
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
}
------------------------------------------------------------
LOGGING SCOPES (POWERFUL AND UNDERUSED)
------------------------------------------------------------
Logging scopes attach metadata to all logs in a block.
using (logger.BeginScope(new Dictionary<string, object>
{
["CorrelationId"] = correlationId
}))
{
logger.LogInformation("Request started");
}
This works perfectly with scoped services.
------------------------------------------------------------
REAL-WORLD EXAMPLES
------------------------------------------------------------
• DbContext → Scoped
• Email sender → Singleton
• Slug generator → Transient
• Request context → Scoped
• Cache engine → Singleton
------------------------------------------------------------
FINAL CHECKLIST
------------------------------------------------------------
• Default to scoped for request-related services
• Use singleton only for stateless, thread-safe services
• Never store request data in singletons
• Use logging scopes for correlation
• Resolve scoped services from InvokeAsync in middleware
Dependency Injection stops being confusing once you see it as:
“A controlled object factory with lifetime rules.”
Once you understand that, DI becomes one of the most powerful tools in .NET.
