C#

Dependency Injection in .NET (Finally Explained): Lifetimes, Middleware, etc.

Dependency Injection (DI) is one of those concepts that almost every .NET developer uses daily—often without fully understanding what’s actually happening under the hood. You register services, inject interfaces, and somehow everything just works. Until it doesn’t. This article is designed to remove the mystery.

Eugene Fischer Feb 05

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. 1. Loose coupling
  2. Your code depends on interfaces, not concrete classes.
  3. 2. Testability
  4. You can inject mocks or fakes without rewriting logic.
  5. 3. Lifetime management
  6. You control how long objects live and when they’re disposed.
  7. 4. Centralized configuration
  8. 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.



✉️ Join the Developer Community

Get the latest insights, tutorials, and tech updates delivered straight to your inbox. No spam, just quality content for developers. Unsubscribe anytime.

Read next

Dependency Injection in .NET (Finally Explained): Lifetimes, Middleware, etc.

Dependency Injection (DI) is one of those concepts that almost every .NET developer uses daily—often without fully understanding what’s actually happening under the hood. You register services, inject interfaces, and somehow everything just works. Until it doesn’t. This article is designed to remove the mystery.

Eugene Fischer Feb 05
An unhandled error has occurred. Reload 🗙