Remigiusz ZalewskiRemigiusz Zalewski

AddSingleton vs AddScoped vs AddTransient in ASP.NET Core Dependency Injection

service-lifetimes

Introduction

When building modern ASP.NET Core applications, Dependency Injection (DI) is a key design pattern that helps you write clean, maintainable, and testable code.
But one question often confuses developers:

❓ What’s the difference between AddSingleton, AddScoped, and AddTransient in ASP.NET Core?

In this article, we’ll break down each service lifetime, explain when to use which, and provide real C# examples you can copy and paste into your projects.


What Is Dependency Injection?

Dependency Injection is a technique where the framework automatically provides (injects) instances of required services into your classes.

In ASP.NET Core, you register services in the DI container (usually in Program.cs) like this:

builder.Services.AddSingleton<IMyService, MyService>();
builder.Services.AddScoped<IMyService, MyService>();
builder.Services.AddTransient<IMyService, MyService>();

But what’s the difference?

Each of these methods defines how long the created service instance should live — in other words, its lifetime.

🧩 AddSingleton — One Instance for the Entire Application

A Singleton service is created once and shared across the entire application lifetime.

That means:

  • The same instance is reused for every request and every dependency.
  • The instance lives until the application stops.

✅ When to use AddSingleton

Use AddSingleton for:

  • Stateless services
  • Configuration providers
  • Logging
  • Caching layers

❌ Avoid when

  • The service holds request-specific data
  • The service depends on a scoped or transient dependency

💡 Example

public interface IGuidService
{
    string GetGuid();
}

public class SingletonGuidService : IGuidService
{
    private readonly string _guid;

    public SingletonGuidService()
    {
        _guid = Guid.NewGuid().ToString();
    }

    public string GetGuid() => _guid;
}

// In Program.cs
builder.Services.AddSingleton<IGuidService, SingletonGuidService>();

Each time you request IGuidService, you get the same instance and the same GUID value.

🌐 AddScoped – One Instance per HTTP Request

AddScoped creates one instance per HTTP request.
All components handling that same request share the same instance, but a new instance is created for each new request.

This makes it ideal for request-specific data and database operations.

✅ Best For

  • Database contexts (DbContext in Entity Framework Core)
  • Unit of Work pattern
  • Request-specific data (services, repositories)

💡 Example

public interface IRequestService
{
    Guid GetRequestId();
}

public class RequestService : IRequestService
{
    private readonly Guid _requestId = Guid.NewGuid();
    public Guid GetRequestId() => _requestId;
}

// Register in Program.cs
builder.Services.AddScoped<IRequestService, RequestService>();

// Example controller
[ApiController]
[Route("api/[controller]")]
public class ScopedExampleController : ControllerBase
{
    private readonly IRequestService _service1;
    private readonly IRequestService _service2;

    public ScopedExampleController(IRequestService service1, IRequestService service2)
    {
        _service1 = service1;
        _service2 = service2;
    }

    [HttpGet]
    public IActionResult Get()
    {
        return Ok(new
        {
            FirstInstance = _service1.GetRequestId(),
            SecondInstance = _service2.GetRequestId()
        });
    }
}

🧠 What you’ll see:

Within the same HTTP request, both FirstInstance and SecondInstance will return the same GUID. When you make another request, a new GUID will appear — confirming a new instance was created.

⚡ AddTransient — A New Instance Every Time

An AddTransient service is created each time it’s requested from the Dependency Injection (DI) container.

Let’s imagine you’re building an email notification system in your ASP.NET Core application.
You might have a lightweight EmailFormatter service that prepares email content before it’s sent.

This service:

  • Doesn’t store state
  • Performs quick, one-off operations
  • Should be short-lived (you don’t want to reuse old email content or data)

That means:

  • Every injection or method call gets a brand new object.
  • This is ideal for lightweight, stateless, and non-expensive services.

✅ When to Use AddTransient

Use AddTransient for:

  • Utility or helper classes
  • Small operations
  • Short-lived, non-expensive services

❌ Avoid When

  • The service performs expensive initialization
  • The service holds shared state or data that needs to persist between calls

💡 Example

public interface IEmailFormatter
{
    string FormatWelcomeEmail(string userName);
}

public class EmailFormatter : IEmailFormatter
{
    public string FormatWelcomeEmail(string userName)
    {
        return $"Hi {userName}, welcome to our platform! 🎉";
    }
}

// Program.cs
builder.Services.AddTransient<IEmailFormatter, EmailFormatter>();

🚀 Why AddTransient Works Here

Each email send operation gets a fresh EmailFormatter. No state is reused between emails. It’s lightweight, stateless, and safe to recreate on demand.

🚀 Summary Table

Here’s a quick reference table summarizing the differences between the three main lifetimes in ASP.NET Core Dependency Injection:

LifetimeCreated...Shared Within RequestShared Across RequestsCommon Use Cases
SingletonOnce (application lifetime)✅ Yes✅ YesLogging, configuration, caching
ScopedOnce per request✅ Yes❌ NoDbContext, request-based services
TransientEvery time it’s requested❌ No❌ NoHelpers, small utility or stateless services

🧠 Key Takeaways

  • Choose Singleton for shared, thread-safe, stateless services.
  • Choose Scoped for request-level services (like DbContext).
  • Choose Transient for lightweight, short-lived dependencies.
  • Correct use of lifetimes ensures performance, stability, and predictable behavior in your ASP.NET Core applications.