Search code examples
c#asp.net-core.net-coreasp.net-core-hosted-services

Start IHostedService after Configure()


I have an .NET Core 3.1 app that serves an endpoint that describes health of application, and an IHostedService crunching through data in database. There's a problem though, the worker function of HostedService starts processing for a long time, and as result the Configure() method in Startup is not called and the /status endpoint is not running.

I want the /status endpoint to start running before the HostedService kicks off. How do i start the endpoint before the Hosted Service?

Sample code

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHostedService<SomeHostedProcessDoingHeavyWork>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/status", async context =>
            {
                await context.Response.WriteAsync("OK");
            });
        });
    }
}

The HostedService

public class SomeHostedProcessDoingHeavyWork : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await MethodThatRunsForSeveralMinutes();
            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }

    private async Task MethodThatRunsForSeveralMinutes()
    {
        // Process data from db....

        return;
    }
}

I tried to explore adding the HostedService in Configure(), but app.ApplicationServices is a ServiceProvider hence readonly.


Solution

  • I ended up using Task.Yield() and implementing an abstract class to encapsulate it, with optional PreExecuteAsyncInternal hook and errorhandler ExecuteAsyncExceptionHandler

    public abstract class AsyncBackgroundService : BackgroundService
    {
        protected ILogger _logger;
        private readonly TimeSpan _delay;
    
        protected AsyncBackgroundService(ILogger logger, TimeSpan delay)
        {
            _logger = logger;
            _delay = delay;
        }
    
        public virtual Task PreExecuteAsyncInternal(CancellationToken stoppingToken)
        {
            // Override in derived class
            return Task.CompletedTask;
        }
    
        public virtual void ExecuteAsyncExceptionHandler(Exception ex)
        {
            // Override in derived class
        }
    
        public abstract Task ExecuteAsyncInternal(CancellationToken stoppingToken);
    
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {   
            // Prevent BackgroundService from locking before Startup.Configure()
            await Task.Yield();
    
            _logger.LogInformation("Running...");
    
            await PreExecuteAsyncInternal(stoppingToken);
    
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    await ExecuteAsyncInternal(stoppingToken);
                    await Task.Delay(_delay, stoppingToken);
                }
                catch (TaskCanceledException)
                {
                    // Deliberate
                    break;
                }
                catch (Exception ex)
                {
                    _logger.LogCritical($"Error executing {nameof(ExecuteAsyncInternal)} in {GetType().Name}", ex.InnerException);
    
                    ExecuteAsyncExceptionHandler(ex);
    
                    break;
                }
            }
    
            _logger.LogInformation("Stopping...");
        }
    }