Differences Between AddTransient, AddScoped, and AddSingleton

By FoxLearn 11/12/2024 1:49:09 AM   14
In ASP.NET Core, AddTransient and AddScoped are two different ways of registering services in the dependency injection container, and they control the lifetime of the services they register.

What is the difference between services.AddTransient and services.AddScoped methods in ASP.NET Core?

For example:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IEmailSender, AuthMessageSender>();
    services.AddScoped<IEmailSender, AuthMessageSender>();
}

Transient objects in ASP.NET Core are always unique; a new instance is created each time they are requested, whether for a controller or any service. This ensures that every dependency receives a fresh object instance.

Scoped objects are created once per HTTP request. They remain the same throughout the duration of the request but are recreated for each new request, ensuring a consistent instance within a single request while providing a fresh instance for subsequent requests.

Singleton objects are created only once and are shared across all requests and throughout the application's lifetime. The same instance is used for every object and request.

To illustrate the differences between service lifetimes and registration options, consider a simple interface representing tasks with a unique `OperationId`. Depending on the service lifetime configuration (Transient, Scoped, or Singleton), the dependency injection container will provide either the same or different instances of the service to the requesting class. To highlight each lifetime, we will define a separate type for each configuration, making it clear which instance is being used in each case.

For example:

public interface IOperation
{
    Guid OperationId { get; }
}

public interface IOperationTransient : IOperation
{
}

public interface IOperationScoped : IOperation
{
}

public interface IOperationSingleton : IOperation
{
}

public interface IOperationSingletonInstance : IOperation
{
}

We implement the interfaces with a single class, `Operation`, which accepts a GUID in its constructor. If no GUID is provided, the class generates a new one. This allows us to demonstrate how different lifetimes affect the creation and management of instances with unique identifiers.

public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton, IOperationSingletonInstance
{
    Guid _guid;
    public Operation() : this(Guid.NewGuid())
    {

    }

    public Operation(Guid guid)
    {
        _guid = guid;
    }

    public Guid OperationId => _guid;
}

In the `ConfigureServices` method, each service type is registered in the dependency injection container according to its designated lifetime (Transient, Scoped, or Singleton), ensuring the appropriate behavior for instance creation and sharing based on the configuration.

services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
services.AddTransient<OperationService, OperationService>();

The `IOperationSingletonInstance` service is registered with a specific instance using a known `Guid.Empty`, making it easy to identify when this type is in use. Additionally, an `OperationService` is registered, which depends on each of the other `Operation` types.

This service helps demonstrate whether the controller is receiving the same instance or a new one for each operation type within a request. The `OperationService` simply exposes its dependencies as properties, allowing their behavior to be displayed in the view for clarity.

public class OperationService
{
    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }

    public OperationService(IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance instanceOperation)
    {
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = instanceOperation;
    }
}

To demonstrate object lifetimes within and across individual requests, the sample includes an `OperationsController` that requests each type of `IOperation` as well as the `OperationService`. The `Index` action then displays the `OperationId` values for both the controller and the service, illustrating how different lifetimes affect the instances provided during the request.

public class OperationsController : Controller
{
    private readonly OperationService _operationService;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationScoped _scopedOperation;
    private readonly IOperationSingleton _singletonOperation;
    private readonly IOperationSingletonInstance _singletonInstanceOperation;

    public OperationsController(OperationService operationService,
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance singletonInstanceOperation)
    {
        _operationService = operationService;
        _transientOperation = transientOperation;
        _scopedOperation = scopedOperation;
        _singletonOperation = singletonOperation;
        _singletonInstanceOperation = singletonInstanceOperation;
    }

    public IActionResult Index()
    {
        // ViewBag contains controller requested services
        ViewBag.Transient = _transientOperation;
        ViewBag.Scoped = _scopedOperation;
        ViewBag.Singleton = _singletonOperation;
        ViewBag.SingletonInstance = _singletonInstanceOperation;

        // Operation service has its own requested services
        ViewBag.Service = _operationService;
        return View();
    }
}

Two separate requests are made to the controller action, allowing us to observe how the service lifetimes behave differently across requests, such as whether the instances remain the same or are recreated for each request.

request one

Request two

request two

By observing the `OperationId` values, we can see how different service lifetimes behave within and across requests:

- Transient objects always have different `OperationId` values, as a new instance is created for every controller and service.
- Scoped objects have the same `OperationId` within a single request but vary between different requests.
- Singleton objects retain the same `OperationId` for every object and every request, regardless of whether an instance is explicitly provided in `ConfigureServices`.