Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 772
morteng Avatar asked May 18 '20 09:05

morteng


2 Answers

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.

like image 170
user1624411 Avatar answered Sep 29 '22 12:09

user1624411


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); 
    }
}
like image 21
pinkfloydx33 Avatar answered Sep 29 '22 11:09

pinkfloydx33