Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What to return from ExecuteAsync for event-driven background tasks?

As part of my REST API I have a BackgroundService that subscribes to a message queue and waits for particular messages to be received and performs certain actions based on the type of message.

In ExecuteAsync I'm currently returning Task.CompletedTask since I don't have anything to 'await', however I'm thinking this is actually incorrect and could be potentially problematic although I'm not sure what exactly how or what problems could arise as a result.

Would it be acceptable to simply 'await' Task.Delay with an infinite delay? Something like:

await Task.Delay(Timeout.infinite, cancellationToken);
like image 598
Astronought Avatar asked Jun 08 '26 06:06

Astronought


1 Answers

TLDR

Essentially it boils down to: Return Task.CompleteTask.

I'd even recommend just using IHostedService directly instead of BackgroundService because you aren't really gaining anything from what I can see.

IHostedService

The first thing to look into is IHostedService. This interface is what BackgroundService implements, and is what enables running code on startup of the host. The interface only adds the StartAsync and StopAsync methods (no ExecuteAsync method).

If we look at the source code for Microsoft.Extensions.Hosting we can see that when we call AddHostedService all it's doing is adding the IHostedService to the IServiceProvider.

public static IServiceCollection AddHostedService<THostedService>(this IServiceCollection services)
    where THostedService : class, IHostedService
{
    services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, THostedService>());

    return services;
}

Then, inside Host.StartAsync it does some startup stuff, but the main thing is how it starts the IHostedService's.

public async Task StartAsync(CancellationToken cancellationToken = default)
{
    // Omitted for brevity 

    _hostedServices = Services.GetService<IEnumerable<IHostedService>>();

    foreach (IHostedService hostedService in _hostedServices)
    {
        // Fire IHostedService.Start
        await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);

        if (hostedService is BackgroundService backgroundService)
        {
            _ = HandleBackgroundException(backgroundService);
        }
    }

    // Omitted for brevity 
}

Why show all this? I think it's important to know there is no real magic happening here. Whenever StartAsync on the host is called, all it does is call StartAsync on the IHostedServices. Conversely StopAsync on the IHostedService is only called when Host.StopAsync is called.

So what makes BackgroundService special?

BackgroundService

You might have noticed there is some special code when starting the IHostedService's checking if it's a BackgroundService. You have already posted a link to the source for BackgroundService, but I'll post the main parts here for visibility.

public virtual Task ExecuteTask => _executeTask;
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
public virtual Task StartAsync(CancellationToken cancellationToken)
{
    // Create linked token to allow cancelling executing task from provided token
    _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

    // Store the task we're executing
    _executeTask = ExecuteAsync(_stoppingCts.Token);

    // If the task is completed then return it, this will bubble cancellation and failure to the caller
    if (_executeTask.IsCompleted)
    {
        return _executeTask;
    }

    // Otherwise it's running
    return Task.CompletedTask;
}

As you can see, it's never actually awaiting on the Task returned from ExecuteAsync. This is what allows us to have ongoing tasks inside a BackgroundService. If you were to try doing a Task.Delay with a huge delay inside a normal IHostedService, your Host would never start properly.

In the situation that you return Task.CompleteTask from ExecuteAsync, it'll just return that complete task back to the StartAsync from Host.

So what's the point of doing all this with BackgroundTask's? Someone correct me if I'm wrong, but from looking at it, it seems like it only really exists for exception handling on long running Tasks. If we look at the HandleBackgroundException inside Host.StartAsync, we see this:

private async Task HandleBackgroundException(BackgroundService backgroundService)
{
    try
    {
        await backgroundService.ExecuteTask.ConfigureAwait(false);
    }
    catch (Exception ex)
    {
        _logger.BackgroundServiceFaulted(ex);
    }
}

All it does it takes the Task from ExecuteAsync and waits it, which allows us to see any exceptions thrown by sending it to the Logger.

Conclusions

Based on what I saw in the source code, I didn't see anything about your case that warrants a BackgroundService.

You've said 'It isn't an asynchronous API and as such doesn't need to awaited', which to me means you don't gain anything from BackgroundService.

You should just use a IHostedService and setup your callbacks in StartAsync, then remove them in StopAsync. There is no reason from what I can see this wouldn't work. It's not like your IHostedService is being stopped because it's not 'running' anymore.

If you wanted to stick with a BackgroundService, it's smarter to return Task.CompleteTask instead of awaiting a delay. The delay will still use some resources to keep the timer, even if that's small.

like image 200
Lolop Avatar answered Jun 10 '26 23:06

Lolop