Asynchronous programming
.NET Core Asynchronous C# Dependency Injection Hosted Services Services Visual Studio

How to Configure Background Tasks with Generic Hosted Services in .NET Core

Welcome to today’s post.

In today’s post I will show how to configure background tasks running within a generic hosted service using .NET Core. The following discussion is applicable to versions of .NET Core 3.0 and beyond.

In a previous post I showed how to create a background worker task in .NET Core.

With a background task we are somewhat limited by what we can do with the task that is run. With the ability to configure the task we can integrate the following features into our tasks:

  1. Database connectivity to application data stores.
  2. Network connectivity to external API services.
  3. General application wide properties including versions.
  4. Connectivity to cloud-based services.
  5. Configuration of application diagnostics and logging.

The above just a sub-set of what we can configure.

Before we can start configuring our application, we will need to install some NuGet packages:

Install the following packages:

Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.Json
Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.Hosting

The Microsoft.Extensions.Configuration package allows us to access configuration sections and key / value pairs within any application configuration file.

The Microsoft.Extensions.Configuration.Json package allows us to explicitly read in configuration settings from any JSON configuration file in our application output folder.

The Microsoft.Extensions.DependencyInjection package allows us to inject instances of objects including configuration settings into any of our application classes, decoupling the source and implementation of the settings retrieval from our classes.

The Microsoft.Extensions.Hosting package allows us to use generic hosting builder to create applications that support hosted worker tasks and start up features.

What I will create is a console application that runs a background task that is polled periodically and fires a database query a specific number of times.

This requires a basic configuration as shown:

{
  "AppSettings": {
    "NumberOfEvents": "10",
    "PollingInterval": "15"
  },
  "ConnectionStrings": {
    "AppDbContext": "Server=localhost;Database=aspnet-BookCatalog;User Id=test;Password=????;"
  }
}

Our console application entry method Main() in program.cs can then be modified to include the generic host builder pipeline. We will make use of the ConfigureService() extension method of the HostBuilder class to expose application context and services.

We do this as follows:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
       	.ConfigureServices((hostContext, services) =>
        {
			// put our configuration code here
		}

The services parameter is what we will use to add interfaces and classes to our service collection to allow our application IOC framework to provide dependency injection for our classes, so they are decoupled from the dependencies in the service collection. 

To obtain configuration keys and values from the appSettings.json file we add the following code into the above code block:

var configBuilder = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appSettings.json", optional: true, reloadOnChange: false);
IConfiguration config = configBuilder.Build();

Now the config variable holds the settings from the appSettings.json configuration file.

For these configurations to be visible in subsequent application service classes, we will need to add it to the IOC container service collection as shown:

services.AddSingleton(config);

With application-wide settings, the accepted lifetime of these settings is while the application instance is running, so we make it a singleton lifetime instance which I discuss in this post on how to use the AddSingleton, AddScope and AddTransient Services. This ensures that the settings do not go out of scope or are released while being accessed in a class.

We next declare a stub for a hosted service LoginEventJobService as shown:

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;

namespace BookLoanEventHubSenderConsoleApp
{
    internal class LoginEventJobService : IHostedService, IDisposable
    {
        private readonly ILogger _logger;

        public LoginEventJobService(IConfiguration configuration, ILogger<LoginEventJobService> logger)
        {
            _logger = logger;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Login Event Background Service is starting.");

            return Task.CompletedTask;
        }

        private void DoWork(object state)
        {
        	// Do some processing here.
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Login Event Service is stopping.");

            return Task.CompletedTask;
        }

        public void Dispose()
        {
        }
    }
}

Once we have the stub background worker service created, we can add it as a hosted service as shown in our host builder application pipeline:

services.AddHostedService<LoginEventJobService>();

Once we have added this line, our program startup configuration will looks as shown:

using System;
using System.IO;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;

namespace BookLoanEventHubSenderConsoleApp
{
    class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    var configBuilder = new ConfigurationBuilder()
                             .SetBasePath(Directory.GetCurrentDirectory())
                             .AddJsonFile("appsettings.json", 
                                 optional: true, reloadOnChange: false);

                    IConfiguration config = configBuilder.Build();

                    services.AddSingleton(config);

                    services.AddHostedService<LoginEventJobService>();
                });
    }
}

Next, to inject the application configuration into our background worker class we use constructor injection as follows:

public LoginEventJobService(IConfiguration configuration, …)

We include some internal class variables to hold our settings parameters as shown:

private readonly ILogger _logger;

private readonly int _pollingInterval;
private readonly int _numberOfEvents;

private int _numberEventsSubmitted;

The configuration settings are extracted as shown in the constructor code body as shown:

public LoginEventJobService(IConfiguration configuration,   
  		ILogger<LoginEventJobService> logger)
{
    _logger = logger;

    this._numberEventsSubmitted = 0;

    this._pollingInterval = 0;

    try
    {
       	this._pollingInterval = configuration.GetSection("AppSettings")
            .GetValue<int>("PollingInterval");
    }
    catch (Exception ex)
    {
       	this._pollingInterval = 30;
    }

    this._numberOfEvents = 0;

    try
    {
       	this._numberOfEvents = configuration
            .GetSection("AppSettings")
            .GetValue<int>("NumberOfEvents");
    }
    catch (Exception ex)
    {
        this._numberOfEvents = 30;
    }
            
    Console.Out.WriteLineAsync($"Polling interval set to {this._pollingInterval}");
    Console.Out.WriteLineAsync($"Number of events set to {this._numberOfEvents}");
}

We will need a timer to control the polling interval and a handler to execute the background task, so we include a declaration for the timer class:

private Timer _timer;

In the StartAsync() method we create an instance of the timer with the polling interval and the event handler, DoWork:

public Task StartAsync(CancellationToken cancellationToken)
{
    _logger.LogInformation("Login Event Background Service is starting.");

    _timer = new Timer(DoWork, null, TimeSpan.Zero, 
    TimeSpan.FromSeconds(this._pollingInterval));

    return Task.CompletedTask;
}

Next we provide a StopAsync() method so that the application is shutdown, the generic hosted service calls this to ensure that other tasks are run to notify and clean up resources as required:

public Task StopAsync(CancellationToken cancellationToken)
{
    _logger.LogInformation("Login Event Service is stopping.");

    _timer?.Change(Timeout.Infinite, 0);

    return Task.CompletedTask;
}

In addition, we will need a Dispose() method if we use a timer resource and our class is implemented with the IDispose interface.

public void Dispose()
{
    _timer?.Dispose();
}

The DoWork() timer event handler, which checks the number of submitted events does not exceed the configured maximum number events and executes a task if it is under the limit, is shown below:

private void DoWork(object state)
{
    if (this._numberEventsSubmitted < this._numberOfEvents)
    {
       	this._numberEventsSubmitted++;
        _logger.LogInformation(
  	        $"Login Event {this._numberEventsSubmitted} Being Retrieved.");
             
       	// Do some processing here.
    }
    else
    {
       	_logger.LogInformation(
  	        "Login Event Service idle. All events retrieved.");
    }
}

We can extend our application to inject additional service classes that run data processing or call API methods and inject the configuration settings into those as well. For example, for a data processing service, we do this using dependency injection as shown:

private readonly ICustomDbService _customDbService;
…

public LoginEventJobService(
    IConfiguration configuration, 
    ICustomDbService customDbService, …)
{
    …
    _customDbService = customDbService;
}

In our DoWork() method, we call our custom data service as follows:

var loginAudits = _customDbService
    .RetrieveNextAuditEntries().GetAwaiter().GetResult();

A typical boilerplate service class to process a data context is shown:

using System;
using System.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace BookLoanEventHubSenderConsoleApp
{
    public class CustomDBService: ICustomDbService 
    {
        private readonly ILogger _logger;
        private readonly ApplicationDbContext _db;

        public CustomDBService(IConfiguration configuration, 
  		ILogger<CustomDBService> logger, 
  		ApplicationDbContext db)
        {
            this._logger = logger;
            this._db = db;
        }

        public void RunDBProcess()
        {
            Console.WriteLine("Running custom DB service within Web Job");
            _logger.LogInformation($"The custom DB service has been run.");
	        // Use the data context _db to read and / or modify data. 
	        …
        }
    }
}

The above will be sufficient to create a useful configurable background hosted worker service using .NET Core.

The above code can be used within either a console application or a windows service.  

That is all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial