morteng
morteng

Reputation: 1392

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.

Upvotes: 17

Views: 16571

Answers (6)

Kyle McClellan
Kyle McClellan

Reputation: 985

The source code of the newer WebApplicationBuilder recommends leveraging ConfigureContainer to achieve this behavior, though I personally don't find that to be the cleanest solution and seems likely it will break in the future.

    /// <summary>
    /// Builds the <see cref="WebApplication"/>.
    /// </summary>
    /// <returns>A configured <see cref="WebApplication"/>.</returns>
    public WebApplication Build()
    {
        // ConfigureContainer callbacks run after ConfigureServices callbacks including the one that adds GenericWebHostService by default.
        // One nice side effect is this gives a way to configure an IHostedService that starts after the server and stops beforehand.
        _hostApplicationBuilder.Services.Add(_genericWebHostServiceDescriptor);
        Host.ApplyServiceProviderFactory(_hostApplicationBuilder);
        _builtApplication = new WebApplication(_hostApplicationBuilder.Build());
        return _builtApplication;
    }

Upvotes: 0

JohnRambo93
JohnRambo93

Reputation: 91

For anyone stumbling over this: Andrew Lock has a very nice solution on his Blog, using the IHostApplicationLifetime:

public class TestHostedService: BackgroundService
{
    private readonly IHostApplicationLifetime _lifetime;
    private readonly TaskCompletionSource _source = new();
    public TestHostedService(IHostApplicationLifetime lifetime)
    {
        _lifetime = lifetime;
        _lifetime.ApplicationStarted.Register(() => _source.SetResult()); 
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _source.Task.ConfigureAwait(false); // Wait for the task to complete!
        await DoSomethingAsync();
    }
}

A potential Problem occours if the application doesn't start up: If the ApplicationStarted token never triggers, then the TaskCompletionSource.Task will never complete, and the ExecuteAsync method will never complete. To solve this problem, you can use this approach:

public class TestHostedService: BackgroundService
{
    private readonly IHostApplicationLifetime _lifetime;
    public TestHostedService(IHostApplicationLifetime lifetime)
    {
        _lifetime = lifetime;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        if (!await WaitForAppStartup(_lifetime, stoppingToken))
        {
            return;
        }

        await DoSomethingAsync();
    }

    static async Task<bool> WaitForAppStartup(IHostApplicationLifetime lifetime, CancellationToken stoppingToken)
    {
        var startedSource = new TaskCompletionSource();
        var cancelledSource = new TaskCompletionSource();

        using var reg1 = lifetime.ApplicationStarted.Register(() => startedSource.SetResult());
        using var reg2 = stoppingToken.Register(() => cancelledSource.SetResult());

        Task completedTask = await Task.WhenAny(startedSource.Task, cancelledSource.Task);

        // If the completed tasks was the "app started" task, return true, otherwise false
        return completedTask == startedSource.Task;
    }
}

Upvotes: 4

morteng
morteng

Reputation: 1392

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...");
    }
}

Upvotes: 1

PaoloTa
PaoloTa

Reputation: 478

I think proposed solutions are a kind of workarounds.

If you add your hosted service inside ConfigureServices(), it will be started before Kestrel because the GenericWebHostService (that in fact runs Kestrel), is added in Program.cs when you call

.ConfigureWebHostDefaults(webBuilder =>
        webBuilder.UseStartup<Startup>()
)

so it's always being added as lasts.

To launch your hosted service after Kestrel, just chain another call to

.ConfigureServices(s => s.AddYourServices()) after the call to ConfigureWebHostDefaults().

Something like this:

IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args)
 .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>())
 .ConfigureServices(s => { 
      s.AddHostedService<SomeHostedProcessDoingHeavyWork>();
  });

and you should be done.

Upvotes: 11

Liam Kernighan
Liam Kernighan

Reputation: 2525

await Task.Yield didn't work for me.

The simplest obvious solution:

Startup.cs

public class Startup
{
   public void ConfigureServices(IServiceCollection services)
   {
      // Implementation omitted
      services.AddSingleton<ApplicationRunState>();
   }

   public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
   {
      // Implementation omitted
      app.MarkConfigurationAsFinished();
   }
}

StartupExtensions.cs

public static void MarkConfigurationAsFinished(this IApplicationBuilder builder)
{
   builder.ApplicationServices.GetRequiredService<ApplicationRunState>()
      .ConfigurationIsFinished = true;
}

ExampleBackgroundService.cs

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        if (!_serviceProvider.GetRequiredService<ApplicationRunState>()
            .ConfigurationIsFinished)
        {
            await Task.Delay(5000);
            continue;
        }

        // Further implementation omitted
    }
}

Upvotes: 0

pinkfloydx33
pinkfloydx33

Reputation: 12739

ExecuteAsync should return a Task and it should do so quickly. From the documentation (emphasis mine)

ExecuteAsync(CancellationToken) is called to run the background service. The implementation returns a Task that represents the entire lifetime of the background service. No further services are started until ExecuteAsync becomes asynchronous, such as by calling await. Avoid performing long, blocking initialization work in ExecuteAsync. The host blocks in StopAsync(CancellationToken) waiting for ExecuteAsync to complete.

You should be able to get around this by moving your logic into a seperate method and awaiting that

protected override async Task ExecuteAsync(CancellationToken stoppingToken) 
{ 
    await BackgroundProcessing(stoppingToken);
}

private async Task BackgroundProcessing(CancellationToken stoppingToken) 
{ 
    while (!stoppingToken.IsCancellationRequested)
    { 
        await MethodThatRunsForSeveralMinutes();
        await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); 
    }
}

Alternatively you might just be able to add an await at the start of the method:

protected override async Task ExecuteAsync(CancellationToken stoppingToken) 
{ 
    await Task.Yield();
    while (!stoppingToken.IsCancellationRequested)
    { 
        await MethodThatRunsForSeveralMinutes();
        await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); 
    }
}

Upvotes: 3

Related Questions