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

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

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

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

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 in GitHub

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

Leave a Comment