← Back to Article List         
Transient vs Scoped vs Singleton

Transient vs Scoped vs Singleton

Published on 27 Sep 2026     5 min read ASP.NET Core MVC
MVC

Service lifetime defines how long an object created by the Dependency Injection container will exist.

Lifetime Object creation Common use
Transient Every time it is requested Lightweight, stateless services
Scoped Once per HTTP request Business services, DbContext
Singleton Once for the entire application Shared, thread-safe services

1. Transient

A new object is created every time the service is requested.

builder.Services.AddTransient<IEmailService, EmailService>();
Request 1:
Controller → Instance A
Service    → Instance B

Request 2:
Controller → Instance C

Use it for:

  • Lightweight services
  • Stateless operations
  • Formatting or calculation services

2. Scoped

One object is created per HTTP request. The same instance is reused throughout that request.

builder.Services.AddScoped<IProductService, ProductService>();
Request 1 → Instance A → reused within Request 1
Request 2 → Instance B → reused within Request 2

Use it for:

  • Business services
  • Repositories
  • EF Core DbContext

AddDbContext() registers DbContext as Scoped by default.

3. Singleton

Only one object is created and shared by every request until the application stops.

builder.Services.AddSingleton<IConfigService, ConfigService>();
Request 1 ─┐
Request 2 ─┼→ Same instance
Request 3 ─┘

Use it for:

  • Application-wide configuration
  • Thread-safe shared services
  • Expensive objects that are safe to reuse

Do not store user-specific data inside a Singleton.

Important rule

Do not inject a Scoped service into a Singleton:

builder.Services.AddSingleton<ReportService>();
builder.Services.AddScoped<AppDbContext>();

A Singleton lives for the application lifetime, but AppDbContext should live for only one request. This creates a captive dependency.

Use:

builder.Services.AddScoped<ReportService>();
builder.Services.AddScoped<AppDbContext>();

Simple memory technique

  • Transient: Every use
  • Scoped: Every request
  • Singleton: Entire application

What problems occur when a Singleton depends on a Scoped service?

When a Singleton depends on a Scoped service, the Scoped service can incorrectly remain alive for the Singleton’s entire lifetime. This is called a captive dependency.

Singleton: Entire application lifetime
    └── Scoped service: Should live for one request

Incorrect example

builder.Services.AddSingleton<ReportService>();
builder.Services.AddScoped<AppDbContext>();
public class ReportService
{
    private readonly AppDbContext _dbContext;

    public ReportService(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }
}

Here, ReportService is created once, but AppDbContext should be created separately for every request.

ASP.NET Core commonly detects this and throws an exception:

Cannot consume scoped service 'AppDbContext'
from singleton 'ReportService'.

Problems

1. Incorrect lifetime

The Scoped service may effectively behave like a Singleton instead of being created per request.

2. Concurrent access

Multiple requests may use the same Scoped instance simultaneously.

DbContext is not thread-safe, so this can cause concurrency exceptions and incorrect behaviour.

3. Stale data

A long-lived DbContext may continue tracking old entities and return outdated data.

4. Memory growth

A long-lived DbContext may keep tracking more entities, increasing memory usage.

5. Disposed-object errors

Depending on how the service is resolved, the Singleton may attempt to use a Scoped object after its scope has been disposed:

ObjectDisposedException

6. User-data leakage

If the Scoped service stores request-specific information, one user’s data could be incorrectly shared with another request.

Recommended solution

Make the consuming service Scoped:

builder.Services.AddScoped<ReportService>();
builder.Services.AddScoped<AppDbContext>();

Now both objects are created once per HTTP request.

If the service must be Singleton

Create a new scope whenever the Scoped service is needed:

public class ReportService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public ReportService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public async Task GenerateAsync()
    {
        await using var scope =
            _scopeFactory.CreateAsyncScope();

        var dbContext =
            scope.ServiceProvider
                 .GetRequiredService<AppDbContext>();

        // Use DbContext only inside this scope
        await dbContext.SaveChangesAsync();
    }
}

For EF Core background work, IDbContextFactory<TContext> is often simpler:

builder.Services.AddDbContextFactory<AppDbContext>();
public class ReportService
{
    private readonly IDbContextFactory<AppDbContext> _factory;

    public ReportService(
        IDbContextFactory<AppDbContext> factory)
    {
        _factory = factory;
    }

    public async Task GenerateAsync()
    {
        await using var dbContext =
            await _factory.CreateDbContextAsync();

        // Use DbContext
    }
}

Key point

A Singleton must not directly capture a Scoped dependency. Make the Singleton Scoped, or create and dispose a separate scope whenever the Scoped service is required.

 

How do you inject services into controllers, views, middleware, and filters?

ASP.NET Core’s built-in DI supports injection into controllers, Razor views, middleware, and filters. No external library is required.

First, register the service in Program.cs:

builder.Services.AddScoped<IProductService, ProductService>();

1. Inject into controllers

Use constructor injection:

public class ProductController : Controller
{
    private readonly IProductService _productService;

    public ProductController(IProductService productService)
    {
        _productService = productService;
    }

    public IActionResult Index()
    {
        var products = _productService.GetProducts();
        return View(products);
    }
}

This is the recommended approach.


2. Inject into Razor views

Use the @inject directive:

@inject IProductService ProductService

@{
    var products = ProductService.GetProducts();
}

You can place commonly used injections in _ViewImports.cshtml:

@inject IConfiguration Configuration

Use view injection mainly for presentation-related services, not complex business logic. Normally, controllers should load data and pass a ViewModel to the view.


3. Inject into middleware

Conventional middleware

Singleton services can be injected into the constructor:

public class LoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<LoggingMiddleware> _logger;

    public LoggingMiddleware(
        RequestDelegate next,
        ILogger<LoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(
        HttpContext context,
        IProductService productService)
    {
        // Scoped service is injected into InvokeAsync
        await _next(context);
    }
}

Register the middleware:

app.UseMiddleware<LoggingMiddleware>();

Important: conventional middleware is created once. Therefore, inject Scoped services into InvokeAsync(), not its constructor.

IMiddleware approach

IMiddleware allows constructor injection of Scoped services:

public class LoggingMiddleware : IMiddleware
{
    private readonly IProductService _productService;

    public LoggingMiddleware(IProductService productService)
    {
        _productService = productService;
    }

    public async Task InvokeAsync(
        HttpContext context,
        RequestDelegate next)
    {
        await next(context);
    }
}

Register it:

builder.Services.AddScoped<LoggingMiddleware>();

app.UseMiddleware<LoggingMiddleware>();

4. Inject into filters

Create a filter with constructor injection:

public class AuditFilter : IActionFilter
{
    private readonly ILogger<AuditFilter> _logger;

    public AuditFilter(ILogger<AuditFilter> logger)
    {
        _logger = logger;
    }

    public void OnActionExecuting(
        ActionExecutingContext context)
    {
        _logger.LogInformation("Action started");
    }

    public void OnActionExecuted(
        ActionExecutedContext context)
    {
        _logger.LogInformation("Action completed");
    }
}

Using ServiceFilter

Register the filter:

builder.Services.AddScoped<AuditFilter>();

Apply it:

[ServiceFilter(typeof(AuditFilter))]
public IActionResult Index()
{
    return View();
}

Using TypeFilter

Explicit registration of the filter itself is not normally required:

[TypeFilter(typeof(AuditFilter))]
public IActionResult Index()
{
    return View();
}

Its dependencies, however, must be registered.

Register globally

builder.Services.AddScoped<AuditFilter>();

builder.Services.AddControllersWithViews(options =>
{
    options.Filters.AddService<AuditFilter>();
});

Quick summary

Location Injection method
Controller Constructor injection
Razor view @inject
Conventional middleware Constructor for Singleton-safe services; InvokeAsync() for Scoped services
IMiddleware Constructor injection
Filter

ServiceFilter, TypeFilter or global registration