11th June 2024

Dependency Injection Lifetimes: Singleton, Scoped, and Transient

Dependency-Injection-Lifetimes-VS-Online-img

Dependency Injection (DI) is a cornerstone of modern .NET development, promoting decoupled architecture and testable code. However, understanding the various service lifetimes—Singleton, Scoped, and Transient—is crucial for using DI effectively. This detailed guide will explore these lifetimes, how to use them, and why they matter. Let's dive deep!

Note: We will be using .NET 6 for this blog.

What is Dependency Injection?

Definition: Dependency Injection is a design pattern that allows a class to receive its dependencies from an external source rather than creating them itself. This promotes a separation of concerns and makes code more modular and testable.

Why Use Dependency Injection?
  • Decoupling: It reduces the dependencies between components, making the system more modular.
  • Testability: It makes unit testing easier because dependencies can be mocked or stubbed.
  • Maintainability: Changes in one part of the system have minimal impact on others.
  • Scalability: It allows easy replacement or extension of components.
How Does Dependency Injection Work in .NET 6?

In .NET 6, DI is typically handled by a built-in container. You register your services and their lifetimes in the Program.cs file, and the container injects these services into your components.

Overview of Service Lifetimes
What Are Service Lifetimes?

Service lifetimes determine how long an instance of a service will be reused by the DI container. There are three main lifetimes in .NET: Singleton, Scoped, and Transient.

1. Singleton

What is a Singleton?

A Singleton service is created once and shared across the entire application lifetime. This means that all components that require this service will receive the same instance.

When to Use Singleton:
  • When you need to maintain a global state or configuration.
  • When the service is resource-intensive to create.
  • When the service should only have one instance to ensure consistency.
Example Use Cases:
  • Configuration Settings: Accessing application-wide configuration values.
  • Caching Mechanisms: Storing data that should be available globally.
  • Logging Services: Centralized logging to a file or database.
Implementation:
                                
                                    
    public class MySingletonService : IMySingletonService
    {
        public string GetData()
        {
            return "Singleton data";
        }
    }
  
                                
                            
Program.cs
                                
                                    
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddSingleton<IMySingletonService, MySingletonService>();
  
                                
                            
Considerations:
  • Ensure thread safety, as the same instance will be accessed by multiple threads.
  • Avoid state that can change over time, as this can lead to unpredictable behavior.
What happens if a Singleton service depends on a Scoped or Transient service?
  • If a Singleton service depends on a Scoped or Transient service, it may lead to unintended behaviors since the dependencies can change, while the Singleton remains the same. It's usually recommended to avoid such dependencies.

2. Scoped

What is a Scoped Service?

A Scoped service is created once per request (or scope). This means that within a single HTTP request, the same instance is used, but a new instance is created for each new request.

When to Use Scoped:
  • When you need to maintain state within a single request.
  • When you want to ensure data consistency throughout the lifecycle of a request.
Example Use Cases:
  • Entity Framework DbContext: Ensures that all database operations within a request are performed in a single context.
  • User Session Management: Storing user-specific data for the duration of a request.
Implementation:
                                
                                    
    public class MyScopedService : IMyScopedService
    {
        public string GetData()
        {
            return "Scoped data";
        }
    }

                                
                            
                                
                                    
  var builder = WebApplication.CreateBuilder(args);
  builder.Services.AddScoped<IMyScopedService, MyScopedService>();

                                
                            
Considerations:
  • Scoped services are perfect for handling request-specific data and operations.
  • Be cautious of memory leaks by properly disposing of resources within the scope.
Can Scoped services be used in non-web applications?
  • Yes, Scoped services can be used in any application type. In non-web applications, you create a scope manually to manage the lifetime of these services.

3. Transient

What is a Transient Service?

A Transient service is created each time it is requested. This means a new instance is provided every time the service is injected.

When to Use Transient:
  • When the service is lightweight and stateless.
  • When you need different instances for different operations.
Example Use Cases:
  • Utility Classes: Small services performing specific tasks.
  • Data Transformation Services: Converting data formats or types.
Implementation:
                                
                                    
  public class MyTransientService : IMyTransientService
    {
        public string GetData()
        {
            return "Transient data";
        }
    }
 
                                
                            
Considerations:
  • Frequent creation and disposal of instances can lead to performance overhead.
  • Ideal for stateless services where retaining state is unnecessary.
What are the performance implications of using Transient services?
  • While creating new instances frequently can impact performance, this is usually negligible for lightweight services. However, for heavier services, consider Singleton or Scoped lifetimes.

Comparing the Lifetimes

Summary of Differences:
  • Singleton: One instance for the entire application lifetime.
  • Scoped: One instance per request.
  • Transient: New instance every time requested.
Choosing the Right Lifetime:
  • Singleton: Use for services that are shared across the application and are thread-safe.
  • Scoped: Use for services that need to maintain consistency within a request but should not be shared across requests.
  • Transient: Use for lightweight, stateless services that can be frequently instantiated.
Tips for Effective DI Usage:
  • Avoid Captive Dependencies: Ensure Singleton services do not depend on Scoped or Transient services.
  • Use Constructor Injection: It's the most common and straightforward way to inject dependencies.
  • Be Mindful of Circular Dependencies: These can cause runtime issues. Use design patterns to avoid them.
Best Practices:
  • Register Interfaces, Not Implementations: This promotes abstraction and makes it easier to swap implementations.
  • Consider Lifetime Impact: Think about the impact of the service lifetime on performance and resource management.
  • Use DI Throughout: Consistently apply DI principles across your application for uniformity and maintainability.

Let's build a simple ASP.NET Core 6 web application that demonstrates the use of Singleton, Scoped, and Transient services. We'll create a simple project with a view to show how these services behave differently.

Our project will have the following structure:
project-structure
Step-by-Step Implementation

1. Create a ASP.NET Core 6 Web Application

2. Define the Service Interfaces

Create the interfaces for our services in the Services directory.

IMyScopedService.cs
                                
                                    
  namespace WebApplication1.Services
    {
        public interface IMyScopedService
        {
            string GetData();
            Guid GetInstanceId();
        }
    }
 
                                
                            
IMySingletonService.cs
                                
                                    
  namespace WebApplication1.Services
    {
        public interface IMySingletonService
        {
            string GetData();
            Guid GetInstanceId();
        }
    }
 
                                
                            
IMyTransientService.cs
                                
                                    
  namespace WebApplication1.Services
    {
        public interface IMyTransientService
        {    
            string GetData();
            Guid GetInstanceId();
        }
    }
 
                                
                            

3. Implement the Services

Implement the services in the Services directory.

MySingletonService.cs
                                
                                    
  namespace WebApplication1.Services
    {
        public class MySingletonService : IMySingletonService
        {
            private readonly Guid _instanceId;

            public MySingletonService()
            {
                _instanceId = Guid.NewGuid();
            }

            public string GetData()
            {
                return "Singleton data";
            }

            public Guid GetInstanceId()
            {
                return _instanceId;
            }
        }
    }
 
                                
                            
MyTransientService.cs
                                
                                    
  namespace WebApplication1.Services
    {
        public class MyTransientService : IMyTransientService
        {
            private readonly Guid _instanceId;

            public MyTransientService()
            {
                _instanceId = Guid.NewGuid();
            }

            public string GetData()
            {
                return "Transient data";
            }

            public Guid GetInstanceId()
            {
                return _instanceId;
            }
        }
    }
 
                                
                            
MyScopedService.cs
                                
                                    
  namespace WebApplication1.Services
  {
      public class MyScopedService : IMyScopedService
      {
          public readonly Guid _instanceId;

          public MyScopedService()
          {
              _instanceId = Guid.NewGuid();
          }

          public string GetData()
          {
              return "Scoped data";
          }

          public Guid GetInstanceId()
          {
              return _instanceId;
          }
      }
  }
 
                                
                            

4. Register the Services in Program.cs

Register the services in the DI container.

Program.cs
                                
                                    
  using WebApplication1.Services;

  var builder = WebApplication.CreateBuilder(args);


  builder.Services.AddSingleton<IMySingletonService, MySingletonService>();
  builder.Services.AddScoped<IMyScopedService, MyScopedService>();
  builder.Services.AddTransient<IMyTransientService, MyTransientService>();

  // Add services to the container.
  builder.Services.AddControllersWithViews();

  var app = builder.Build();

  // Configure the HTTP request pipeline.
  if (!app.Environment.IsDevelopment())
  {
  app.UseExceptionHandler("/Home/Error");
  // The default HSTS value is 30 days. You may want to change
  // this for production scenarios, see https://aka.ms/aspnetcore-hsts.
  app.UseHsts();
  }

  app.UseHttpsRedirection();
  app.UseStaticFiles();

  app.UseRouting();

  app.UseAuthorization();

  app.MapControllerRoute(
  name: "default",
  pattern: "{controller=Home}/{action=Index}/{id?}");

  app.Run();
 
                                
                            

5. Create a ViewModel

Create a ViewModel to hold the service data in the Models directory.

ServiceUsageViewModel.cs
                                
                                    
  namespace WebApplication1.Models
    {
        public class ServiceUsageViewModel
        {
            public string SingletonData { get; set; }
            public Guid SingletonId { get; set; }

            public string ScopedData1 { get; set; }
            public Guid ScopedId1 { get; set; }
            public string ScopedData2 { get; set; }
            public Guid ScopedId2 { get; set; }

            public string TransientData1 { get; set; }
            public Guid TransientId1 { get; set; }
            public string TransientData2 { get; set; }
            public Guid TransientId2 { get; set; }
        }
    }
 
                                
                            

6. Create the Home Controller

Create a controller to use these services and pass data to the view.

HomeController.cs
                                
                                    
    using Microsoft.AspNetCore.Mvc;
    using System.Diagnostics;
    using WebApplication1.Models;
    using WebApplication1.Services;

    namespace WebApplication1.Controllers
    {
        public class HomeController : Controller
        {
            private readonly IMySingletonService _singletonService;
            private readonly IMyScopedService _scopedService1;
            private readonly IMyScopedService _scopedService2;
            private readonly IMyTransientService _transientService1;
            private readonly IMyTransientService _transientService2;

            public HomeController(IMySingletonService singletonService,
                                  IMyScopedService scopedService1,
                                  IMyScopedService scopedService2,
                                  IMyTransientService transientService1,
                                  IMyTransientService transientService2)
            {
                _singletonService = singletonService;
                _scopedService1 = scopedService1;
                _scopedService2 = scopedService2;
                _transientService1 = transientService1;
                _transientService2 = transientService2;
            }

            public IActionResult Index()
            {
                var viewModel = new ServiceUsageViewModel
                {
                    SingletonData = _singletonService.GetData(),
                    SingletonId = _singletonService.GetInstanceId(),

                    ScopedData1 = _scopedService1.GetData(),
                    ScopedId1 = _scopedService1.GetInstanceId(),
                    ScopedData2 = _scopedService2.GetData(),
                    ScopedId2 = _scopedService2.GetInstanceId(),

                    TransientData1 = _transientService1.GetData(),
                    TransientId1 = _transientService1.GetInstanceId(),
                    TransientData2 = _transientService2.GetData(),
                    TransientId2 = _transientService2.GetInstanceId()
                };

                return View(viewModel);
            }
        }
    }
 
                                
                            

7. Create the View

Create a view to display the data in the Views/Home directory.

Index.cshtml
                                
                                    
    using Microsoft.AspNetCore.Mvc;
    using System.Diagnostics;
    using WebApplication1.Models;
    using WebApplication1.Services;

    namespace WebApplication1.Controllers
    {
        public class HomeController : Controller
        {
            private readonly IMySingletonService _singletonService;
            private readonly IMyScopedService _scopedService1;
            private readonly IMyScopedService _scopedService2;
            private readonly IMyTransientService _transientService1;
            private readonly IMyTransientService _transientService2;

            public HomeController(IMySingletonService singletonService,
                                  IMyScopedService scopedService1,
                                  IMyScopedService scopedService2,
                                  IMyTransientService transientService1,
                                  IMyTransientService transientService2)
            {
                _singletonService = singletonService;
                _scopedService1 = scopedService1;
                _scopedService2 = scopedService2;
                _transientService1 = transientService1;
                _transientService2 = transientService2;
            }

            public IActionResult Index()
            {
                var viewModel = new ServiceUsageViewModel
                {
                    SingletonData = _singletonService.GetData(),
                    SingletonId = _singletonService.GetInstanceId(),

                    ScopedData1 = _scopedService1.GetData(),
                    ScopedId1 = _scopedService1.GetInstanceId(),
                    ScopedData2 = _scopedService2.GetData(),
                    ScopedId2 = _scopedService2.GetInstanceId(),

                    TransientData1 = _transientService1.GetData(),
                    TransientId1 = _transientService1.GetInstanceId(),
                    TransientData2 = _transientService2.GetData(),
                    TransientId2 = _transientService2.GetInstanceId()
                };

                return View(viewModel);
            }
        }
    }
 
                                
                            

Explanation and Demonstration

Singleton Service:
  • The MySingletonService is registered as a Singleton, meaning the same instance is shared throughout the entire application lifetime. Regardless of how many times the HomeController is instantiated or how many requests are made, the Singleton service will always return the same instance ID.
Scoped Service:
  • The MyScopedService is registered as Scoped, meaning a new instance is created for each HTTP request. Within a single request, all references to MyScopedService will use the same instance. However, for different requests, different instances will be used.
Transient Service:
  • The MyTransientService is registered as Transient, meaning a new instance is created every time it is requested. This means even within a single HTTP request, each time MyTransientService is injected, a new instance is used.

Running the Application

  • Run the application.
  • Navigate to the home page.
  • Refresh the page multiple times.
You will observe the following:
  • Singleton Service: The instance ID remains the same across all requests.
  • Scoped Service: The instance IDs for ScopedService1 and ScopedService2 will be the same within a single request but will change with each new request.
  • Transient Service: The instance IDs for TransientService1 and TransientService2 will be different every time they are requested, even within the same request.

Conclusion

Mastering Dependency Injection —Singleton, Scoped, and Transient—enhances the flexibility, performance, and maintainability of your applications. By understanding when and how to use each lifetime, you can ensure that your services are utilized efficiently, promoting a well-structured and robust application architecture.

Dependency Injection is more than just a pattern; it's a philosophy that, when applied correctly, leads to cleaner, more manageable, and more testable code. So, take the time to understand it deeply, and you'll see the benefits in your .NET applications.

Let's develop your ideas into reality