This post is part 3 of a 3-part series on dependency injection in .NET 6. You might want to read Part 1 and Part 2 first.

To wrap up our series on Dependency Injection (DI) in .NET 6, let's discuss how individual dependencies are created by the container. The manner in which this happens is called the dependency's service lifetime.

There are three service lifetimes implemented by .NET 6:

  • Transient
  • Scoped
  • Singleton

Each of these has a different use case, and each fits a particular kind of dependency. Let's start with the most common service lifetime: transient.

The Sample Project

As always, there's a sample project hosted on GitHub that we used to create this post. Check it out!

GitHub - exceptionnotfound/NET6DependencyInjectionDemo
Contribute to exceptionnotfound/NET6DependencyInjectionDemo development by creating an account on GitHub.

Transient (New Instance Every Time)

Dependencies declared with the transient service lifetime will have a new instance created by the container every time they are injected into another object.

We give a dependency a transient service lifetime using the method AddTransient<T> in the Program.cs file:

builder.Services.AddTransient<IMovieRepository, MovieRepository>();

The first item (IMovieRepository) is the abstraction, and the second item (MovieRepository, no I) is the implementation to use for that abstraction. In this way, we could theoretically have two different implementations for any given abstraction, and use AddTransient<T> to specify the implementation we want to be injected by the container.

Now imagine that we have two page model classes, MoviePageModel and MovieDetailsPageModel, each of which take IMovieRepository as a dependency.

using Microsoft.AspNetCore.Mvc.RazorPages;

namespace DependencyInjectionNET6Demo.Pages;

public class MoviesPageModel : PageModel
{
    private readonly IMovieRepository _movieRepo;
    public List<Movie> Movies { get; set; } = new List<Movie>();

    public MoviesPageModel(IMovieRepository movieRepo)
    {
        _movieRepo = movieRepo;
    }

    public void OnGet()
    {
        //Implementation
    }
}
Implementation shortened for brevity
public class MovieDetailsModel : PageModel
{
    private readonly IMovieRepository _movieRepo;
    
    public MovieDetailsModel(IMovieRepository movieRepo)
    {
        _movieRepo = movieRepo;
    }

    public void OnGet(int id)
    {
        Movie = _movieRepo.GetByID(id);
    }
}
Implementation shortened for brevity

Each time these two classes are instantiated, they will receive a new instance of MovieRepository. Even if these two classes are instantiated as part of the same HTTP request, they will still receive different instances.

Most of the time, dependencies that are declared transient are going to be very lightweight, and have a limited number of dependencies themselves. Further, it is assumed that dependencies marked transient will not manage their own state; this condition is a large part of what makes them transient.

Transient is considered the "default" service lifetime in .NET 6. This means that you should make all dependencies transient unless they truly need to use one of the other service lifetimes.

Scoped (Once Per Application Request)

Dependencies declared as scoped are created once per application request. "Application request" differs in different kinds of apps; in ASP.NET web apps, "application request" is an HTTP web request.

The dependency instance is created at the beginning of the request, injected into all dependencies that need it during the request, and disposed of by the container at the end of the request.

We declare a dependency as scoped using the AddScoped<T> method:

builder.Services.AddScoped<ICustomLogger, CustomLogger>();

In order to understand this lifetime better, let's use an abstraction ICustomLogger, with the implementation CustomLogger:

namespace DependencyInjectionNET6Demo.Services.Interfaces;

public interface ICustomLogger
{
    void Log(Exception ex);
    void Log(string info);
}
using DependencyInjectionNET6Demo.Services.Interfaces;

namespace DependencyInjectionNET6Demo.Services;

public class CustomLogger : ICustomLogger
{
    public void Log(Exception ex)
    {
        //Implementation
    }

    public void Log(string info)
    {
        //Implementation
    }
}

Our MovieRepository requests ICustomLogger in its constructor:

namespace DependencyInjectionNET6Demo.Repositories;

public class MovieRepository : IMovieRepository
{
    private readonly IActorRepository _actorRepo;
    private readonly ICustomLogger _logger;

    public MovieRepository(IActorRepository actorRepo, ICustomLogger logger)
    {
        _actorRepo = actorRepo;
        _logger = logger;
    }
    
    //...Rest of implementation
}

Now, let's imagine we have a new Razor Page called MovieDetails.cshtml, which uses the class MovieDetailsModel as its code-behind file:

using DependencyInjectionNET6Demo.Services.Interfaces;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace DependencyInjectionNET6Demo.Pages;

public class MovieDetailsModel : PageModel
{
    private readonly IMovieRepository _movieRepo;
    private readonly ICustomLogger _logger;
    public Movie Movie = new();

    public MovieDetailsModel(IMovieRepository movieRepo, 
                             ICustomLogger logger)
    {
        _movieRepo = movieRepo;
        _logger = logger;
    }

    public void OnGet(int id)
    {
        Movie = _movieRepo.GetByID(id);
        _logger.Log("GET action fired!");
    }

    public void OnPost()
    {
        _logger.Log("POST action fired!");
    }
}

In other words: MovieDetailsModel depends on ICustomLogger AND IMovieRepository, and MovieRepository also depends on ICustomLogger. When dependencies are injected, both MovieDetailsModel and MovieRepository will receive the same instance of CustomLogger, since it was added to the container as a scoped dependency and these objects are injected during the same HTTP request.

(Though it may look like it at first glance, this is not a circular dependency.)

Why would we use a scoped dependency? Often these types of dependencies are ones that might be relatively expensive or time-consuming to create but need to be updated often. In many of my team's apps, information about the currently logged-in user is added to the container as a scoped dependency, because it is relatively expensive to create (and may involve a round-trip to the database) but it is needed often and can change frequently.

Another example of something we might want to be a scoped dependency would be information about the HTTP request itself, such as cookies. This information must exist for the duration of the request, but must also be usable for each subsequent request.

Singleton (One Instance, Almost Forever)

A singleton is a dependency that is created once by the container on application startup and then injected into every dependent class that needs an instance of it. That instance will exist until the application is shut down or restarted (Microsoft explicitly says we should not implement code to manage a singleton service's lifetime and should let the container handle that for us).

In the real world, one example of a singleton dependency might be a cache service, which maintains a memory cache and accessors for that cache.

public interface ICacheService
{
    void Add(string key, object value);
    T Get<T>(string key);
}
using Microsoft.Extensions.Caching.Memory;

namespace DependencyInjectionNET6Demo.Services;

public class CacheService : ICacheService
{
    private readonly IMemoryCache _cache;

    public CacheService(IMemoryCache cache)
    {
        _cache = cache;
    }

    public void Add(string key, object value)
    {
        _cache.Set(key, value);
    }

    public T Get<T>(string key)
    {
        return _cache.Get<T>(key);
    }
}
For an alternate implementation of this for the real world, see this StackOverflow answer.

The reason we'd want this CacheService class to be a singleton is to ensure that every dependent class is accessing and modifying the same instance of IMemoryCache.

Singleton dependencies can manage their own internal state; since the dependency is only created once, the internal state of the instance can change and be maintained. This is what makes them distinct from either transient or scoped dependencies.

Summary

Dependencies can have one of three service lifetimes:

  • Transient services are created every time they are injected. This is the default service lifetime.
  • Scoped services are created once per request.
  • Singleton services are created only once, the same instance gets injected to every dependent class.

What service lifetime each dependency should have depends on several things, among them the internal state that each dependency should maintain. By default, injectable services should be transient and only use the scoped or singleton service lifetimes if their use case demands such a lifetime.

Thanks for reading our series on Dependency Injection in .NET 6!

Happy Coding!