ASP.NET – How to use a BackgroundService for long-running and periodic tasks

In ASP.NET, when you need a long-running background task, or need to run a task periodically, you can implement IHostedService or BackgroundService and register your class as a hosted service in Startup.

As you may have already encountered, if you try to run a long-running background task, the web server will randomly kill your task because it’s not associated with a current request. This is why you need to use a implement a hosted service to take care of this work – the web server won’t randomly kill it.

Fortunately, it’s quite simple to add a hosted service in ASP.NET. In this article, I’ll show how to create and registered a hosted background service. This background service pings Google every 30 seconds and logs the result of the ping.

1 – Create hosted service class by inheriting from BackgroundService

To create a hosted service you have two options:

  • Implement IHostedService from scratch.
  • Inherit from BackgroundService – an abstract base class that implements IHostedService.

In this article I’m going to be inheriting from BackgroundService.

The first step is to create a class that inherits from BackgroundService, override the ExecuteAsync() method, and add async to the method signature.

  • using Microsoft.Extensions.Hosting;
public class PingerService : BackgroundService { protected async override Task ExecuteAsync(CancellationToken stoppingToken) { //todo } }
Code language: C# (cs)

2 – Register the hosted service class

In Startup.ConfigureServices, I need to use AddHostedService() to register the hosted service. Don’t use AddSingleton().

public class Startup { //other methods public void ConfigureServices(IServiceCollection services) { //other service registrations services.AddHostedService<PingerService>(); } }
Code language: C# (cs)

Now when you run this, it’ll call your hosted service’s ExecuteAsync() method and it’ll stay running in the background.

Note: Do an await right away in your ExecuteAsync() call, otherwise it’ll block the Startup code. You can put await Task.Yield() at the beginning of ExecuteAsync() if you want to make sure to not to block Startup.

3 – Implement ExecuteAsync() in the hosted service

By inheriting from BackgroundService, you really only need to worry about implementing ExecuteAsync(). The main thing to remember here that you make your async call pay attention to the passed in CancellationToken.

In this example, I am making this periodically ping a url by using the Ping class.

Ping.SendPingAsync() doesn’t accept a CancellationToken, so I need to create a second task using Task.Delay() and pass in the CancellationToken to that, then use Task.WhenAny() to await both. The second task will throw an exception if the CancellationToken is cancelled.

Again, the key to remember is to make your async calls in ExecuteAsync() pay attention to the CancellationToken.

public class PingerService : BackgroundService { private readonly Ping Pinger; private readonly ILogger Logger; private readonly IPingSettings PingSettings; public PingerService(ILogger logger, IPingSettings pingSettings) { PingSettings = pingSettings; Pinger = new Ping(); Logger = logger; } protected async override Task ExecuteAsync(CancellationToken stoppingToken) { while(!stoppingToken.IsCancellationRequested) { await Task.Delay(PingSettings.Frequency, stoppingToken); try { var pingTask = Pinger.SendPingAsync(PingSettings.Target, (int)PingSettings.Timeout.TotalMilliseconds); var cancelTask = Task.Delay(PingSettings.Timeout, stoppingToken); //double await so exceptions from either task will bubble up await await Task.WhenAny(pingTask, cancelTask); if(pingTask.IsCompletedSuccessfully) { LogPingReply(pingTask.Result); } else { LogError("Ping didn't complete successfully"); } } catch(Exception ex) { LogError(ex.Message); } } } private void LogPingReply(PingReply pingReply) { Logger.Information($"PingReply status={pingReply.Status} roundTripTime={pingReply.RoundtripTime}"); } private void LogError(string error) { Logger.Error(error); } public override void Dispose() { if(Pinger != null) { Pinger.Dispose(); } base.Dispose(); } }
Code language: C# (cs)

4 – Wire up the dependencies in Startup.ConfigureServices

In PingerService I am dependency injecting two dependencies through the constructor – ILogger and IPingSettings. So I need to wire these up in Startup.ConfigureServices().

I am using Serilog as the logger and hardcoding the log file path.

public class Startup { //other methods public void ConfigureServices(IServiceCollection services) { //other service registrations var seriFileLogger = new LoggerConfiguration().WriteTo.File(@"C:\Temp\Logs\log.txt").CreateLogger(); services.AddSingleton<Serilog.ILogger>(seriFileLogger); services.AddSingleton<IPingSettings>(new PingSettings() { Timeout = TimeSpan.FromSeconds(5), Frequency = TimeSpan.FromSeconds(30), Target = "www.google.com" }); services.AddHostedService<PingerService>(); } }
Code language: C# (cs)

5 – Results – run the web API and verify the background service is working

Normally when you launch a web API, it does nothing unless there are requests. However, in this case, I have a background service running. It’s pinging every 30 seconds and writing the outcome to a log file.

Sure enough, if I look in C:\Temp\Logs\log.txt, I can see it’s logging every 30 seconds.

2020-08-20 07:50:05.466 -04:00 [INF] PingReply status=Success roundTripTime=39 2020-08-20 07:50:35.532 -04:00 [INF] PingReply status=Success roundTripTime=40 2020-08-20 07:51:05.595 -04:00 [INF] PingReply status=Success roundTripTime=50 2020-08-20 07:51:35.657 -04:00 [INF] PingReply status=Success roundTripTime=39
Code language: plaintext (plaintext)

Code in GitHub

The full code used in this article is available here: https://github.com/makolyte/aspdotnet-backgroundpinger

3 thoughts on “ASP.NET – How to use a BackgroundService for long-running and periodic tasks”

  1. Background service lifetimes depend on the App Pool’s “idle time out” setting. If your app pool has an “idle timeout” of 5 mins your service will stop after 5 mins and won’t restart. Therefore a request to the api in this example must be made within every 5 mins.

    Reply
    • Thanks David!

      When using IIS as the web server, it’s typically recommended to set your idle timeout to 0. This is especially true if you have a background service. In addition, be aware of app pool recycles. By default, IIS recycles your app pool every 29 hours. This attempts to do a graceful shutdown, which calls StopAsync() on all registered background services.

      So if you want your background service to try to do a graceful shutdown, you can override BackgroundService.StopAsync().

      Reply
      • Thanks David and Mak. Could not figure out why my background service kept stopping. Do not see any mention of this in the Hosted Service docs… But anyway, I set my idle time to 0 and my job is now running every 24hrs.

        Reply

Leave a Comment