A2Z.Net : B – BackgroundService

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.

Worker Service 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.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s