2

I successfuly implemented background task for my ASP.NET Core 2.1 Web App, and suddenly today it has stopped working, giving me an error:

System.InvalidOperationException: Cannot consume scoped service 'MyDbContext' from singleton 'Microsoft.Extensions.Hosting.IHostedService'

I know there are few topics about that issue and I've read what this error is about, but I think my worker is implemented correctly. I'm creating scoped service and it's been working successfuly until today. My implementation is analogous to the one from this tutorial: https://shortly.cc/EIxe

Backgroud task code:

public class OnlineTagger : Core.BackgroundService
    {
        private readonly CoordinatesHelper _coordinatesHelper;

        public OnlineTagger(IServiceScopeFactory scopeFactory, CoordinatesHelper coordinatesHelper) : base(scopeFactory)
        {
            _coordinatesHelper = coordinatesHelper;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {

            while (!stoppingToken.IsCancellationRequested)
            {
                using (var scope = _scopeFactory.CreateScope())
                {
                    var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();

                    Console.WriteLine("Online tagger Service is Running");

                    // Run something
                    await ProcessCoords(dbContext);

                    // Run every 30 sec
                    await Task.Delay(30000, stoppingToken);
                }
            }
        }

        private async Task ProcessCoords(MyDbContext dbContext)
        {
            var topCoords = await _coordinatesHelper.GetTopCoordinates();

            foreach (var coord in topCoords)
            {
                var user = await dbContext.Users.SingleOrDefaultAsync(c => c.Id == coord.UserId);

                if(user != null)
                {
                    var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                    //expire time = 120 sec
                    var coordTimeStamp = DateTimeOffset.FromUnixTimeMilliseconds(coord.TimeStamp).AddSeconds(120).ToUnixTimeMilliseconds();

                    if (coordTimeStamp < now && user.IsOnline == true)
                    {
                        user.IsOnline = false;
                        await dbContext.SaveChangesAsync();
                    }
                    else if(coordTimeStamp > now && user.IsOnline == false)
                    {
                        user.IsOnline = true;
                        await dbContext.SaveChangesAsync();
                    }
                }
            }
        }
    }

Base class:

    public abstract class BackgroundService : IHostedService, IDisposable
{
    protected readonly IServiceScopeFactory _scopeFactory;
    private Task _executingTask;
    private readonly CancellationTokenSource _stoppingCts =
                                                   new CancellationTokenSource();

    public BackgroundService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        // Store the task we're executing
        _executingTask = ExecuteAsync(_stoppingCts.Token);

        // If the task is completed then return it,
        // this will bubble cancellation and failure to the caller
        if (_executingTask.IsCompleted)
        {
            return _executingTask;
        }

        // Otherwise it's running
        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        // Stop called without start
        if (_executingTask == null)
        {
            return;
        }

        try
        {
            // Signal cancellation to the executing method
            _stoppingCts.Cancel();
        }
        finally
        {
            // Wait until the task completes or the stop token triggers
            await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite,
                                                          cancellationToken));
        }
    }

    public virtual void Dispose()
    {
        _stoppingCts.Cancel();
    }
}

Startup.cs:

services.AddSingleton<Microsoft.Extensions.Hosting.IHostedService, OnlineTagger>();
Maciej Wanat
  • 1,527
  • 1
  • 11
  • 23
  • Message is correct. Singleton services can't used scoped services because they are disposed of after they go out of scope. They have a shorter life span then the singleton which can pose a problem. – Nkosi Jul 31 '18 at 15:20
  • That is exactly why I am creating a new one inside of a background task. It is explained here: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1#consuming-a-scoped-service-in-a-background-task – Maciej Wanat Jul 31 '18 at 15:24
  • What happens if you add `OnlineTagger` as transient? The main issue is that `OnlineTagger` is a singleton. Under the hood `AddHostedService` adds the host to the service collection as transient. – Nkosi Jul 31 '18 at 15:27
  • There is even a big warning in the [documentation](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.1#service-lifetimes) `Warning: It's dangerous to resolve a scoped service from a singleton. It may cause the service to have incorrect state when processing subsequent requests.` – Nkosi Jul 31 '18 at 15:30
  • I get very similar, yet not the same error: `System.InvalidOperationException: 'Cannot consume scoped service 'MyDbContextt' from singleton 'Microsoft.AspNetCore.Hosting.Internal.HostedServiceExecutor'.'` Still I think worker should be a singleton anyways. – Maciej Wanat Jul 31 '18 at 15:36

0 Answers0