Start IHostedService after Configure()
Asked Answered
N

6

17

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.

Nesbitt answered 18/5, 2020 at 9:19 Comment(3)
Try moving the body of ExecuteAsync into a seperate method and then await that in ExecuteAsync ie await DoWork(stoppingToken). You can also try placing await Task.Delay(1); as the first line. Creation of services are blocked on until they become asynchronous. I'd expect it to become so immediately at your first await, but similar examples in the docs use the first mechanism I suggestDielectric
It worked by adding Task.Delay() before executing worker task. Will you post as answer?Nesbitt
Maybe I'm missing something but neither of the solutions below worked for me. I had to add service in the Program.cs instead of inside the Startup.ConfigureServices. Otherwise the server didn't actually start.Jeter
N
1

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...");
    }
}
Nesbitt answered 19/5, 2020 at 10:44 Comment(1)
the await Task.Yield(); line needs to be the first line in the method. The reason for it is to "release" the Task so that Host.StartAsync can continue. If any preceding code blocks (yes, even awaited stuff can block) then the Host will block at this service and not continue with starting any others. In your snippet... you will get this behavior as a side effect, assuming that PreExecuteAsyncInternal can return async. Putting it in the loop "works" but just putting it as line one will give you the behavior you're looking for.Boche
R
11

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.

Rabbinism answered 29/12, 2021 at 13:35 Comment(2)
Could this answer be updated for .NET 6+? The sample code provided doesn't exist anymore now that there's no explicit Startup.cs (at least, by default)Ungenerous
It works perfectly. For those who need to implement the code above, I suggest to rewrite .NET 6/7 (the "Minimal Hosting") to old style, as the new is cosmetic and the vast amount of docs are in old style. Also, I noticed they didn't worry to make "scape doors", so we can have access to extending at full force. So, for advanced scenarios, it seems we are stuck in the old style.Lissotrichous
D
4

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;
    }
}
Demmer answered 9/3, 2023 at 11:45 Comment(2)
This is a great solution.. especially for more recent versions of NETSzombathely
> A potential Problem occours if the application doesn't start up When would that happen? Also what about await _source.Task.WaitAsync(cancellationToken);?Rhine
D
3

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); 
    }
}
Dielectric answered 18/5, 2020 at 15:30 Comment(0)
N
1

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...");
    }
}
Nesbitt answered 19/5, 2020 at 10:44 Comment(1)
the await Task.Yield(); line needs to be the first line in the method. The reason for it is to "release" the Task so that Host.StartAsync can continue. If any preceding code blocks (yes, even awaited stuff can block) then the Host will block at this service and not continue with starting any others. In your snippet... you will get this behavior as a side effect, assuming that PreExecuteAsyncInternal can return async. Putting it in the loop "works" but just putting it as line one will give you the behavior you're looking for.Boche
G
0

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
    }
}
Guss answered 21/12, 2020 at 17:1 Comment(1)
Your solution works, but there is a better form of Task.Delay. Make use of SemaphoreSlim which releases immediately.Vaginitis
C
0

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;
    }

Contrayerva answered 12/10, 2023 at 18:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.