“Dependency Injection and Lifetimes” over a gradient background

Dependency Injection And Lifetimes

In software development, particularly in the context of dependency injection (DI), the lifetimes of services determine how long an instance of a class is kept alive. Let’s define them.

There are 3 lifetimes possible when using dependency injection: Transient, Scoped and Singleton.

Let’s detail each of these with a definition, its respective use cases and a case example.

Transient

If you need a new instance every time you request the service, this lifetime suits your need.

It’s what we call stateless services where each operation is independent of any previous operations.

Code Example of a Transient

Let’s take a very trivial example with C# programming:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public interface ITransientService
{
    void Execute();
}

public class TransientService : ITransientService
{
    public void Execute()
    {
        Console.WriteLine("Transient service executed.");
    }
}

// Registering the service, usually in the Program.cs
services.AddTransient<ITransientService, TransientService>();

Common Use Cases for a Transient

Generally, the common use cases are the following:

  • Utility Services, which don’t need to maintain any state between calls.
  • Formatting Services, such as converting data into a specific string format.
  • Calculation Services, when you need to perform computations or data transformations without needing to maintain any state.
  • Operation Services, which perform a specific operation independently each time they’re called, like sending an email or logging an event.

Of course, all these use cases becomes relevant if you require class instantiation for the service.

Read more about Transient

If you need to dive deeper into the topic, you can find below some online sources:

Scoped

When you need to maintain a state during a single request but not across multiple requests, using a scoped dependency becomes the proper lifetime to use.

In a web application, we create a new instance for each HTTP request, and we can use throughout that request lifetime.

Common Use Cases for a Scoped

Here are some common use cases that you can find out there:

  • Entity Framework DbContext, which ensures a single instance of the DbContext used throughout a request to manage database operations, thus avoiding concurrency issues and ensuring that all changes are tracked and persisted correctly.
  • Unit of Work Pattern, when managing a single unit of work that encompasses multiple repository operations within a single request.
  • Caching per Request, to cache data that is expensive to fetch or compute and should be reused within the same request but not beyond it.
  • Request-Specific Data: Storing request-specific data, such as user authentication information, that requires access by multiple components during the request processing.

Code Example of a Scoped

Let’s consider a scenario where we have a web application that handles user authentication and authorization. We need to access user-specific data multiple times during a single request to ensure that the user has the correct permission to access various resources.

We can use a scoped service to store and manage this user data.

  1. Let’s create a UserContextService and its interface:

    It holds user-specific data for the duration of a single request.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    
    public interface IUserContextService
    {
        void SetUserId(string userId);
        void SetUserName(string name);
        void SetRoles(List<string> roles);
        string GetUserId();
        string GetUserName();
        List<string> GetRoles();
    }
    
    public class UserContextService : IUserContextService
    {
        private string _userId;
        private string _userName;
        private List<string> _roles;
    
        public string SetUserId(string userId) {
         this._userId = userId;
        }
        public string SetUserName(string name) {
         this._name = name;
        }
        public List<string> SetRoles(List<string> roles) {
         this._roles = roles;
        }
    
    
        public string GetUserId() {
         return this._userId;
        }
        public string GetUserName() {
         return this._name;
        }
        public List<string> GetRoles() {
         return this._roles;
        }
    }
    
  2. Next, let’s register the Scoped Service:

    In an IocHelper class (or wherever you configure your services that you call from Program.cs), register the UserContextService with a scoped lifetime.

    Below, we use an extension method registering the UserContextService.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    public class IocHelper
    {
        public void ConfigureServices(this IServiceCollection services)
        {
            services.AddScoped<IUserContextService, UserContextService>();
    
            // Other service registrations...
        }
    
        // Other methods...
    }
    
  3. We continue with the implementation of a Middleware to populate UserContextService

    We create a middleware to populate the UserContextService with user data at the beginning of each request.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    public class UserContextMiddleware
    {
        private readonly RequestDelegate _next;
    
        public UserContextMiddleware(RequestDelegate next)
        {
            _next = next;
        }
    
        public async Task InvokeAsync(HttpContext context, IUserContextService userContextService)
        {
            // Simulate fetching user data, typically from an authentication service
            // This is where you could retrieve data from an IAM like Keycloak
            userContextService.SetUserId("123");
            userContextService.SetUserName("JohnDoe");
            userContextService.SetRoles("Admin");
    
            await _next(context);
        }
    }
    
  4. Then, you register the middleware:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    public class AppConfigurationHelper
    {
    		// Register the middleware
    		public void RegisterMiddlwares(this IApplicationBuilder app)
    		{
    		    app.UseMiddleware<UserContextMiddleware>();
    		    // Other middleware registrations...
    		}
    }
    
  5. Finally, use UserContextService in a Controller.

    In a controller, you can inject the IUserContextService and use it to access user-specific data during the request.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    public class HomeController : Controller
    {
        private readonly IUserContextService _userContextService;
    
        public HomeController(IUserContextService userContextService)
        {
            _userContextService = userContextService;
        }
    
        public IActionResult Index()
        {
            var userName = _userContextService.GetUserName();
            var roles = string.Join(", ", _userContextService.GetRoles());
    
            return Content($"Hello {userName}, you have the following roles: {roles.Join(',')}");
        }
    }
    

Singleton

If you need to maintain shared state across the entire application lifetime, using a Singleton is a good choice.

It’s created once on the first request or at application startup. Every subsequent request will use the same instance.

Common Use Cases of a Singleton

You will find that using a Singleton commonly falls into the following use cases:

  • Configuration Services, which provide application-wide configuration settings read once and used throughout the application’s lifetime.
  • Logging Services: Centralized logging services that need to maintain a single instance to collect and process log entries from various parts of the application.
  • Caching Services, that need to cache data globally to avoid repeating expensive operations, such as fetching static data or configuration from a database.

Example of a Singleton

The most common usage will be our example: we have a web application that needs to log activities across different modules. We want to use a single logging service that collects and processes all log entries, ensuring that log data is centralized and managed efficiently.

  1. Let’s start with the implementation of a LoggingService

    The LoggingService below logs messages to a centralized log store.

    The service should be a singleton to ensure that all components use the same instance.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    public interface ILoggingService
    {
        void Log(string message);
    }
    
    public class LoggingService : ILoggingService
    {
        private readonly List<string> _logs = new List<string>();
    
        public void Log(string message)
        {
            _logs.Add(message);
            Console.WriteLine($"Log entry added: {message}");
        }
    
        public IEnumerable<string> GetLogs()
        {
            return _logs;
        }
    }
    
  2. Next, we register the Singleton Service

    In an IocHelper class (or wherever you configure your services that you call from Program.cs), register the LoggingService with a singleton lifetime.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    public class IocHelper
    {
        public void ConfigureServices(this IServiceCollection services)
        {
            services.AddSingleton<ILoggingService, LoggingService>();
    
            // Other service registrations...
        }
    
        // Other methods...
    }
    
  3. Finally, use LoggingService in Controllers

    In a controller, you can inject the ILoggingService into the constructor and use it to log messages.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    public class HomeController : Controller
    {
        private readonly ILoggingService _loggingService;
    
        public HomeController(ILoggingService loggingService)
        {
            _loggingService = loggingService;
        }
    
        public IActionResult Index()
        {
            _loggingService.Log("HomeController.Index accessed.");
            return Content("Index page accessed.");
        }
    
        public IActionResult About()
        {
            _loggingService.Log("HomeController.About accessed.");
            return Content("About page accessed.");
        }
    }
    

Summary

Use Transient for stateless services that can be recreated as needed and don’t hold a state.

Use scoped for services that should be unique to a single request or scope, holding state that shouldn’t persist beyond that scope.

Use Singleton for services that need to maintain a state across the entire application lifecycle and all requests and users can access it through the various services.

I hope this short summary helped you understand the usage of each lifetime next time you work a new service for your applications.

Follow me

Thanks for reading this article. Make sure to follow me on X, subscribe to my Substack publication and bookmark my blog to read more in the future.

Licensed under CC BY-NC-SA 4.0
License GPLv3 | Terms
Built with Hugo
Theme Stack designed by Jimmy