Long-running background processes have been a need for long and for over the years, this has been accomplished in various approaches in .Net. With the introduction of Worker Service templates, it has become a lot easier to create asynchronous long-running background tasks that are supported across platforms and have inbuilt capabilities like Dependency Injection.
The core of the Worker Service revolves around BackgroundService
.
In this post, we will attempt to achieve the following
- Understand Background Service and IHostedService
- What is Worker Service Template
- Host a long running background task in a Windows Service
- Host a long running background task in a Linux Daemon
- Host a long running background task in a .Net Core Web Api
Let us begin our journey.
BackgroundService
So what exactly is the BackgroundService
class ? In .Net Core 2.0, Dot Net team introduced an interface IHostedService
.
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
The interface comprises of two methods
- StartAsync : This method is called when the application host is ready to start the service.
- StopAsync : This method is triggered when application is performing graceful shutdown.
As you would have noticed, the interface just provides an entry and exit point for initiating your background task code. How you actually ensure it runs for a long time would be the developer’s choice (loops/timers..)
BackgroundService
is an abstract class that was introduced in .Net Core 3.0 which implements the IHostedService interface. This makes it easier for developers to create hosted services. While it isn’t quite needed to create a Hosted Service, it is worth taking a look at the implementation of BackgroundService
so that you could understand the inner working of the abstract class.
public abstract class BackgroundService : IHostedService, IDisposable
{
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Store the task we're executing
_executingTask = ExecuteAsync(_stoppingCts.Token);
// If the task is completed then return it, this will bubble cancellation and failure to the caller
if (_executingTask.IsCompleted)
{
return _executingTask;
}
// Otherwise it's running
return Task.CompletedTask;
}
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
// Stop called without start
if (_executingTask == null)
{
return;
}
try
{
// Signal cancellation to the executing method
_stoppingCts.Cancel();
}
finally
{
// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
}
public virtual void Dispose()
{
_stoppingCts.Cancel();
}
}
Since .Net Core 3.0, Hosted Services could be easily created by inheriting your service class from the BackgroundService
abstract class. StartAsync
method invokes an abstract method ExecuteAsync
which would be ideal for writing our custom logic required for the background task.
Worker Service Template
As mentioned earlier, the Worker Service Templates provide an easy-to-use platform (code template) for creating your hosted service. Let us create our Worker Service Project using the template.
This would create a Project (console application) with two files.
- Program.cs
- Worker.cs
The Program.cs
file comprises of following code.
using WorkerServiceDemo;
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync();
All it does is to create an instance of IHostBuilder
and configure the required services. We use the IServiceCollection.AddHostedService<T>()
method to add/register our background service instance.
The Worker.cs
contains the background service class, which would be where all the magic would be happening. The default template of the class would be as follows.
namespace WorkerServiceDemo
{
public class Worker : BackgroundService
{
private ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
}
}
The Worker
class, as expectedly inherit from the BackgroundService
abstract class. As mentioned earlier, since dependency injection is supported by default, we are using injecting the instance of ILogger
using DI here.
The ExecuteAsync
method as expectedly contrains our logic. In the sample genarated, it executes a loop on the CancellationToken
, and adds a text to the logger after 1000ms delay. There isn’t much happening here, but this would be a good place to get a basic understanding of the BackgroundService
. Later in this post, we will create a Web Api and use hosted service to monitor a message broker.
Windows Service
The hosted service which you created could be easily converted to a windows service with minimal code changes ( or rather I should say – one line). But to do that you need to add a nuget package.
Microsoft.Extensions.Hosting.WindowsServices
This would provide the extension methods to support Windows Services. Now all you need to do is to add one line in your code to create IHost
instance.
IHost host = Host.CreateDefaultBuilder(args)
.UseWindowsService() // This is the line to add
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
})
.Build();
The UseWindowsService
method sets the host lifetime to WindowsServiceLifetime, sets the Content Root, and enables logging to the event log with the application name as the default source name. This is definitely a lot easier to do than the way we used to build Windows Service earlier (and a hell of a lot less code).
Linux Daemon
On the other hand, if you need to use it as a Linux Daemon, things aren’t quite different. You need to use a different package though.
Microsoft.Extensions.Hosting.Systemd
And then, instead of UseWindowsService
, you would need to use another method UseSystemd
. Infact, if you are unsure what the underlying OS platform could be, you could chain the calls.
IHost host = Host.CreateDefaultBuilder(args)
.UseWindowsService() // execute if Windows
.UseSystemd() // If Linux
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
})
.Build();
As you noticed, you needn’t explicitly add any guard clauses to check if the underlying operating systems and type of host process. In fact, this is done by the UseWindowsService
and UseSystemd
methods internally.
public static IHostBuilder UseWindowsService(this IHostBuilder hostBuilder, Action<WindowsServiceLifetimeOptions> configure)
{
if (WindowsServiceHelpers.IsWindowsService())
{
// Do rest of configuration
}
return hostBuilder;
}
public static bool IsWindowsService()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return false;
}
var parent = Internal.Win32.GetParentProcess();
if (parent == null)
{
return false;
}
return parent.SessionId == 0 && string.Equals("services", parent.ProcessName, StringComparison.OrdinalIgnoreCase);
}
// UseSystemd
public static IHostBuilder UseSystemd(this IHostBuilder hostBuilder)
{
if (SystemdHelpers.IsSystemdService())
{
// do rest of configuration
}
return hostBuilder;
}
public static bool IsSystemdService()
=> _isSystemdService ?? (bool)(_isSystemdService = CheckParentIsSystemd());
private static bool CheckParentIsSystemd()
{
// No point in testing anything unless it's Unix
if (Environment.OSVersion.Platform != PlatformID.Unix)
{
return false;
}
try
{
// Check whether our direct parent is 'systemd'.
int parentPid = GetParentPid();
string ppidString = parentPid.ToString(NumberFormatInfo.InvariantInfo);
byte[] comm = File.ReadAllBytes("/proc/" + ppidString + "/comm");
return comm.AsSpan().SequenceEqual(Encoding.ASCII.GetBytes("systemd\n"));
}
catch
{
}
return false;
}
** Hosting in a Web Api **
The hosted services aren’t restricted to desktop services. You could also host them in a .Net Core Web Api. For example, one of the common scenarios that might arise in a microservice is to constantly monitor a message broker, other than exposing a host of Web Api end points.
We could achieve this by hosting the hosted service in a Web Api. As expected, the Service needs to implement the IHostedService
interface. Let us write our service first.
public class QueueConsumerService:BackgroundService
{
private const string CommandQueueName = "UserCommandQueue";
private IConnection _connection;
private IModel _channel;
public QueueConsumerService()
{
InitializeQueue();
}
private void InitializeQueue()
{
var factory = new ConnectionFactory
{
HostName = "localhost",
};
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
_channel.QueueDeclare(queue: CommandQueueName, true,false,false,null);
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
var consumer = new EventingBasicConsumer(_channel);
consumer.Received += (s, a) =>
{
var replyQueueName = a.BasicProperties.ReplyTo;
var messageFromClient = Encoding.UTF8.GetString(a.Body.ToArray());
var message = $"Mock Reply from the Background Service : Reply for '{messageFromClient}'";
var body = Encoding.UTF8.GetBytes(message);
_channel.BasicPublish(exchange:"",routingKey:replyQueueName,null,body);
};
_channel.BasicConsume(CommandQueueName, true, consumer);
return Task.CompletedTask;
}
}
The next step is, as expected to configure the Web Api to run our service as hosted service.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Configure your Background Service
builder.Services.AddHostedService<QueueConsumerService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
As you can see, there isn’t too much difference in the whole approach now. You could literally host a service on a Windows Service or Linux Daemon or a Web Api using almost the same set of code. The whole effort to achieve them has come down considerably and has become uniform across platforms.
We will continue our deep dives on some of the features in .Net/C# in this series in upcoming posts.