Search code examples
c#dependency-injectionservice

Is there a chance my scoped service will get disposed before the scope ends?


I'm trying to create a service that will run in the background and run a timer task at a certain interval. I can't add it as a hosted service because it needs to only start running after another hosted service has done its startup, so I'm creating a scope within that hosted service and running this one by getting it as a scoped service:

    services.AddScoped<TaskTimer>();
    services.AddHostedService<BotService>();

This timer service is implemented as:

using Microsoft.Extensions.Logging;

namespace MyBot;

internal class TaskTimer : IDisposable {
    #region Private vars

    private readonly ILogger<TaskTimer> _log;

    #endregion

    #region Constructors

    public TaskTimer(ILogger<TaskTimer> log) {
        _log = log;

        _log.LogInformation("Starting task timer");

        // [start timer here]
    }

    #endregion

    public void Dispose() {
        _log.LogInformation("Stopping task timer");

        // [stop timer here]
    }
}

... then within the main bot service:

public class BotService : BackgroundService {
    protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
        // [do init here...]

        using (var botServiceScope = _serviceProvider.CreateScope()) {
            // Start task timer
            _ = botServiceScope.ServiceProvider.GetRequiredService<TaskTimer>();

            // [await stoppingToken]
        }
    }
}

This seems to work as I'm testing it now, but I'm wondering whether there's a chance the task timer could get disposed before the scope using statement closes, because I'm throwing away the return value from GetRequiredService. Is it guaranteed to only get disposed when the scope closes even though I'm not holding a reference to the service instance?


Solution

  • Is there a chance my scoped service will get disposed before the scope ends? because I'm throwing away the return value from GetRequiredService

    "Throwing away" will not lead to call of the Dispose method, GC does not care about class implementing IDisposable, it cares only about finalizers (though finalizer can share implementation or directly call Dispose). IDisposable.Dispose is only called "explicitly" (either via direct call or via using). In case of the DI (build-in one) - it will dispose and "dereference" the scoped dependency only after the containing scope is disposed, it will preserve the reference until that (it does not know anything about the visibility scopes and it need to maintain the single instance per scope guarantee).

    I can't add it as a hosted service because it needs to only start running after another hosted service has done its startup.

    You actually can. Just provide some synchronization mechanism (similar to the example in the docs for caching):

    // synchronization primitive, wrap in some meaningful class:
    builder.Services.AddSingleton(new SemaphoreSlim(0, 1)); 
    builder.Services.AddHostedService<MyServ2>();
    builder.Services.AddHostedService<MyServ1>();
    
    public class MyServ1 : BackgroundService
    {
        private readonly SemaphoreSlim _semaphoreSlim;
    
        public MyServ1(SemaphoreSlim semaphoreSlim)
        {
            _semaphoreSlim = semaphoreSlim;
        }
    
        public override async Task StartAsync(CancellationToken cancellationToken)
        {
            // init sync/async
            Console.WriteLine("Starting 1");
            await Task.Delay(100);
            Console.WriteLine("Started 1");
            _semaphoreSlim.Release();
            await base.StartAsync(cancellationToken);
        }
    
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                Console.WriteLine(nameof(MyServ1));
                await Task.Delay(100);
            }
        }
    }
    
    public class MyServ2 : BackgroundService
    {
        private readonly SemaphoreSlim _semaphoreSlim;
    
        public MyServ2(SemaphoreSlim semaphoreSlim)
        {
            _semaphoreSlim = semaphoreSlim;
        }
    
        public override async Task StartAsync(CancellationToken cancellationToken)
        {
            await _semaphoreSlim.WaitAsync();
            Console.WriteLine("starting 2");
            
            await base.StartAsync(cancellationToken);
        }
    
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                Console.WriteLine(nameof(MyServ2));
                await Task.Delay(100);
            }
        }
    }
    

    Second approach (which I would not recommend though and it is provided only for completeness) is to rely on the internal workings of the hosted services - register services in correct order and perform blocking initialization of the first one:

    builder.Services.AddHostedService<MyServ1>();
    builder.Services.AddHostedService<MyServ2>();
    
    public class MyServ1 : BackgroundService
    {
        public override async Task StartAsync(CancellationToken cancellationToken)
        {
            // blocking init:
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(100);
                Console.WriteLine("starting 1");
            }
            
            await base.StartAsync(cancellationToken);
        }
    
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                Console.WriteLine(nameof(MyServ1));
                await Task.Delay(100);
            }
        }
    }
    
    public class MyServ2 : BackgroundService
    {
        public override async Task StartAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("starting 2");
            
            await base.StartAsync(cancellationToken);
        }
    
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                Console.WriteLine(nameof(MyServ2));
                await Task.Delay(100);
            }
        }
    }