Click here to Skip to main content
16,018,006 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more:
I have a .NET 8 web api that has a HostedService and a controller. The controller uses an interface that the HostedService also implements so that the service may be started, stopped, or restarted. By default, the intent is for no intervention.
The StartAsync method is using a PeriodicTimer so that work will be attempted every 15 seconds.

The issue I have is that when an attempt is made to stop the service, it does not stop. I have tried everything I can think of and was hoping to see if anyone can point out what I am doing incorrectly. When I say it does not stop, after the periodictimer ticks expire, another cycle of work is started. CancellationToken should be thread safe and yet it is not catching that the work is cancelled.

Code

Program.cs
C#
using HostedService.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddSingleton<IControllableBackgroundService, UserOfficeHostedService>();
builder.Services.AddHostedService<UserOfficeHostedService>();

builder.Services.Configure<HostOptions>(x =>
{
    x.ServicesStartConcurrently = true;
    x.ServicesStopConcurrently = false;
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseRouting();
app.UseCors(x => x
    .AllowAnyOrigin()
    .AllowAnyMethod()
    .AllowAnyHeader()
);

app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();


IControllableBackgroundService
C#
namespace HostedService.Services;
    
    public interface IControllableBackgroundService
    {
        Task StartServiceAsync();
        Task StopServiceAsync();
        Task RestartServiceAsync();
    }


BackgroundServiceController
C#
using HostedService.Services;
    using Microsoft.AspNetCore.Mvc;
    
    namespace HostedService.Controllers;
    [ApiController]
    [Route("[controller]")]
    public class BackgroundServiceController(IControllableBackgroundService backgroundService) : ControllerBase
    {
        [HttpGet]
        [Route("start")]
        public async Task<IActionResult> StartService()
        {
            await backgroundService.StartServiceAsync();
            return Ok();
        }
    
        [HttpGet]
        [Route("stop")]
        public async Task<IActionResult> StopService()
        {
            await backgroundService.StopServiceAsync();
            return Ok();
        }
    
        [HttpGet]
        [Route("restart")]
        public async Task<IActionResult> RestartService()
        {
            await backgroundService.RestartServiceAsync();
            return Ok();
        }
    
    }


UserOfficeHostedService
C#
namespace HostedService.Services;
    
    public class UserOfficeHostedService(
        ILogger<UserOfficeHostedService> logger) : IHostedService, IControllableBackgroundService, IDisposable
    {
        private CancellationTokenSource cts = new();
        private PeriodicTimer timer = new (TimeSpan.FromSeconds(15)));
    
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            logger.LogInformation("starting service");
            cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    
            while (await timer.WaitForNextTickAsync(cts.Token))
            {
                if (cts.Token.IsCancellationRequested)
                    break;
    
                await UpdateUserOfficeCacheAsync(cts.Token);
            }
        }
    
        public async Task StopAsync(CancellationToken cancellationToken)
        {
            logger.LogInformation("stopping service");
    
            await cts.CancelAsync();
            timer.Dispose();
        }
    
        public async Task StartServiceAsync()
        {
            if (!cts.IsCancellationRequested) 
                return;
    
            timer = new PeriodicTimer(TimeSpan.FromSeconds(serviceInterval));
            await StartAsync(cts.Token);
        }
    
        public async Task StopServiceAsync()
        {
            await cts.CancelAsync();
            await StopAsync(CancellationToken.None);
        }
    
        public async Task RestartServiceAsync()
        {
            await StopAsync(cts.Token);
            timer = new PeriodicTimer(TimeSpan.FromSeconds(serviceInterval));
            await StartAsync(cts.Token);
        }
    
        private async Task UpdateUserOfficeCacheAsync(CancellationToken cancellationToken)
        {
            logger.LogInformation($"Performing update on UserOffice Cache. Time: {DateTimeOffset.Now}");
    
            try
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    logger.LogInformation($"Cancellation received before work attempted. Time: {DateTimeOffset.Now}");
                    return;
                }
    
                // Simulate task
                await Task.Delay(16000, cancellationToken); // Replace this with actual long-running task logic
    
                if (cancellationToken.IsCancellationRequested)
                {
                    logger.LogInformation($"Cancellation received after work started. Time: {DateTimeOffset.Now}");
                }
            }
            catch (TaskCanceledException)
            {
                logger.LogInformation("UserOffice Cache update was canceled.");
            }
            finally
            {
                logger.LogInformation("UserOffice Background Service released semaphore.");
            }
        }
    
        public void Dispose()
        {
            timer?.Dispose();
            cts?.Dispose();
        }
    }


Results
info: HostedService.Services.UserOfficeHostedService[0]
          starting service
    info: Microsoft.Hosting.Lifetime[14]
          Now listening on: https://localhost:7016
    info: HostedService.Services.UserOfficeHostedService[0]
          Performing update on UserOffice Cache. Time: 7/16/2024 4:41:51 PM -04:00
    info: HostedService.Services.UserOfficeHostedService[0]
          stopping service
    info: HostedService.Services.UserOfficeHostedService[0]
          UserOffice Background Service released semaphore.
    info: HostedService.Services.UserOfficeHostedService[0]
          Performing update on UserOffice Cache. Time: 7/16/2024 4:42:07 PM -04:00


As you can see, the service was stopped and then more work started. How do I fix this?

What I have tried:

I have tried the BackgroundService and now the IHostedService with various incarnations of CancellationTokenSource and CancellationToken without success. The service keeps restarting.
Additionally, various timers have been used as well.
Posted
Updated 16-Jul-24 12:47pm
v3

That is not how you do background services. You can read more here: Background tasks with hosted services in ASP.NET Core | Microsoft Learn[^]
 
Share this answer
 
Comments
Lee Zeitz 2024 17-Jul-24 8:29am    
Please be specific about what is actually incorrect. The Microsoft Learn article is being followed, though I need to do more on the IDisposable front.
The issue is in the registration of the service

The following needs to be replaced because it will register 2 different instances of UserOfficeHostedService, one as IControllableBackgroundService and another as IHostedService, causing management of different instances compared to one running as a hosted service.

Replace
C#
builder.Services.AddSingleton<IControllableBackgroundService, UserOfficeHostedService>();
builder.Services.AddHostedService<UserOfficeHostedService>();


New Code
C#
builder.Services.AddSingleton<UserOfficeHostedService>();
builder.Services.AddSingleton<IControllableBackgroundService>(sp => sp.GetRequiredService<UserOfficeHostedService>());
builder.Services.AddHostedService(sp => sp.GetRequiredService<UserOfficeHostedService>());
 
Share this answer
 

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

  Print Answers RSS
Top Experts
Last 24hrsThis month


CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900