Dependency Injection in .NET (C#) – Deep Dive into Lifetimes, Internals, Real-World Usage & Interview Scenarios

Introduction

Dependency Injection (DI) is not just another design pattern in .NET — it is the foundation of modern ASP.NET Core architecture.
Every controller, service, middleware, and even the framework itself depends on DI.

Most developers know what DI is.
Senior developers are expected to understand how it works internally, why lifetimes matter, and what breaks when used incorrectly.

This post explains DI from the inside out — not just definitions.


What Dependency Injection REALLY Means (Beyond Definition)

At its core, Dependency Injection means:

A class should not be responsible for creating the objects it depends on.

Without DI (Tight Coupling – Real Problem)

public class OrderService
{
    private readonly EmailService _email = new EmailService();
}

Problems here:

  • OrderService is tightly coupled to EmailService
  • Cannot replace EmailService
  • Cannot mock for unit testing
  • Any change in EmailService affects OrderService

This design does not scale.


With Dependency Injection (Loose Coupling – Scalable Design)

public class OrderService
{
    private readonly IEmailService _email;

    public OrderService(IEmailService email)
    {
        _email = email;
    }
}

Now:

  • OrderService depends on an abstraction
  • Implementation can change without touching OrderService
  • Unit testing becomes easy
  • Architecture becomes flexible

👉 This is Dependency Injection in action


DI vs DIP (Critical Interview Concept)

Many developers mix these up.

Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Dependency Injection (DI)

The mechanism that supplies those abstractions at runtime.

🧠 Key line for interviews

DIP is the rule, DI is the technique that enforces it.


How Dependency Injection Works Internally in ASP.NET Core

ASP.NET Core has a built-in IoC container.

When the app starts:

  1. Services are registered in IServiceCollection
  2. The framework builds a Service Provider
  3. At runtime, dependencies are resolved automatically
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserService, UserService>();

When a controller needs IUserService, the container:

  • Checks registration
  • Creates instance based on lifetime
  • Injects it automatically

Dependency Injection Lifetimes (IN-DEPTH – VERY IMPORTANT)

This is where most real bugs happen.


🔹 Transient Lifetime (New Instance Every Time)

What it REALLY Means

A brand-new object is created every time the dependency is requested — even within the same request.

services.AddTransient<ILogService, LogService>();

If injected:

  • Into two services → two different instances
  • Into the same service twice → two instances

When Transient Is Good

  • Stateless services
  • Lightweight helpers
  • Formatters, mappers, validators

Hidden Danger

If Transient depends on:

  • DbContext
  • Heavy objects
  • File or network resources

➡ It causes performance issues


🔹 Scoped Lifetime (Most Important & Most Used)

What Scoped REALLY Means

One instance is created per HTTP request, and shared across that request only.

services.AddScoped<IOrderService, OrderService>();

Real Request Flow

1 HTTP request comes in
➡ One scoped instance is created
➡ Used across controllers/services
➡ Disposed at request end

Why Scoped Is Perfect for Web Apps

  • Safe for concurrent users
  • Request-specific data stays consistent
  • Automatically disposed

Best Real-World Use Cases

DbContext
✔ Business services
✔ Repositories
✔ Unit of Work

🧠 Golden Rule

In ASP.NET Core, Scoped should be your default choice unless you have a reason not to.


🔹 Singleton Lifetime (Most Dangerous if Misused)

What Singleton REALLY Means

Only ONE instance for the entire application lifetime.

services.AddSingleton<ICacheService, CacheService>();

Shared across:

  • All requests
  • All users
  • All threads

When Singleton Is GOOD

  • Configuration
  • In-memory caching
  • Read-only services
  • Stateless helpers

When Singleton BREAKS Apps

❌ Storing request data
❌ Using DbContext
❌ Using HttpContext

CRITICAL Interview Rule

A Singleton must NEVER depend on Scoped or Transient services.

Why?

  • Scoped object gets disposed
  • Singleton keeps reference
  • Causes runtime crashes

Lifetime Dependency Rule (INTERVIEW GOLD)

Depends On ↓Allowed?
Singleton → Singleton
Scoped → Singleton
Scoped → Scoped
Transient → Anything
Singleton → Scoped
Singleton → Transient

Memorize this table.


Dependency Injection with Repository & Unit of Work (REAL PROJECT SETUP)

Repository

public interface IUserRepository
{
    User Get(int id);
}

Unit of Work

public interface IUnitOfWork
{
    IUserRepository Users { get; }
    void Save();
}

DI Registration (Correct Way)

services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();

✔ Same DbContext
✔ Same transaction
✔ Clean architecture


Constructor Injection (WHY IT IS STRONGLY RECOMMENDED)

public class PaymentService
{
    private readonly ILogger _logger;

    public PaymentService(ILogger logger)
    {
        _logger = logger;
    }
}

Why Constructor Injection Wins

  • Dependency is mandatory
  • Object is always valid
  • Supports immutability
  • Easy to test

🧠 Interview line

If a dependency is required for the class to work, it belongs in the constructor.


Property & Method Injection (When to Use)

These are secondary options, not defaults.

Property Injection

  • Optional dependencies
  • Feature toggles

Method Injection

  • Short-lived operations
  • One-time logic

Avoid overuse — they hide dependencies.


DI and Unit Testing (BIG ADVANTAGE)

var mockRepo = new Mock<IUserRepository>();
var service = new UserService(mockRepo.Object);

Without DI:

  • You cannot mock
  • Tests become integration tests

With DI:

  • Fast unit tests
  • Isolated logic
  • Cleaner test suites

Common DI Mistakes (Seen in Interviews)

❌ Service Locator Pattern

serviceProvider.GetService<IService>();
  • Hides dependencies
  • Breaks testability

❌ Injecting Concrete Classes

Always inject interfaces.

❌ Using Singleton DbContext

Causes threading & data corruption issues.


DI vs Factory Pattern (Real Answer)

  • DI manages object lifetime & wiring
  • Factory controls creation logic

👉 In real systems:

DI creates the Factory, Factory creates the object.

They complement each other.


When NOT to Use Dependency Injection

  • Small scripts
  • Very simple console apps
  • Performance-critical hot loops

DI is powerful — not mandatory everywhere.


Conclusion

Dependency Injection is the backbone of modern .NET architecture.
Understanding lifetimes, dependency rules, and real-world usage is what separates junior developers from senior engineers.

If you understand DI deeply:

  • Your designs improve
  • Your bugs reduce
  • Your interviews become easier

Read our detailed guides on Design Patterns

Explore more important concepts here…

2 thoughts on “Dependency Injection in .NET (C#) – Deep Dive into Lifetimes, Internals, Real-World Usage & Interview Scenarios”

  1. Pingback: Middleware vs Filters in ASP.NET Core – Differences & Examples

  2. Pingback: ASP.NET Core Request Pipeline – Complete Execution Order

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top