Strange C# Tricks 8B: Background service- made even easier.

Michael Berezin
4 min readMar 4, 2024

--

Strange C# Tricks is a series of articles about some cool and obscure tricks I learned over my career. They may be helpful only in some specific scenarios, but learning new ideas is never a bad thing.

Several months ago I wrote an article about background services in ASP.net. A few days ago I showed it to a co-worker, and he surprised me by finding some issues with my solution:

  1. I was sending work to the background service from the controller and not the business layer.
  2. My solution was not easy to unit-test.
  3. My solution was not generic.

So I got back to the drawing board and came up with something better.

A more generic solution:

The first step was to find a way to have a settings object that can be used to call multiple services.

public class BackgroundTaskSettings
{
public BackgroundTaskSettings(string name, JObject settings)
{
Name = name;
Settings = settings;
}

public string Name { get; set; }
public JObject Settings { get; set; }
}

I choose to pass the settings using a JObject and not using the generic T, as it is easier to register things in a DI, without adding a lot of complexity.

And a container that will hold the tasks that we need to be executed:

public class TasksToRun
{
private readonly ConcurrentQueue<BackgroundTaskSettings> _tasks = new();

public TasksToRun() => _tasks =
new ConcurrentQueue<BackgroundTaskSettings>();

public void Enqueue(BackgroundTaskSettings settings) =>
_tasks.Enqueue(settings);

public void Enqueue(List<BackgroundTaskSettings> settingsList)
{
foreach (var settings in settingsList)
{
_tasks.Enqueue(settings);
}
}

public BackgroundTaskSettings? Dequeue()
{
var hasTasks = _tasks.TryDequeue(out var settings);
return hasTasks ? settings : null;
}

public bool IsEmpty => _tasks.IsEmpty;
}

The next step was to create an interface for all the concrete services that will do all the work, so they can be used by the background service

public interface IBackgroundServiceable
{
string Name();

Task Execute(BackgroundTaskSettings settings);
}

And finally the background service itself:

public class GenericBackgroundTaskService : Microsoft.Extensions.Hosting.BackgroundService
{
private readonly List<IBackgroundServiceable> _backgroundServiceableList;
private readonly TasksToRun _tasks;

//TODO - get number of seconds from config
private readonly TimeSpan _timeSpan = TimeSpan.FromSeconds(1);

public GenericBackgroundTaskService(TasksToRun tasks,
IEnumerable<IBackgroundServiceable> backgroundServiceableList)
{
_tasks = tasks;
_backgroundServiceableList = backgroundServiceableList.ToList();
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using PeriodicTimer timer = new(_timeSpan);
while (!stoppingToken.IsCancellationRequested &&
await timer.WaitForNextTickAsync(stoppingToken))
{
await ExecuteTasksFromQueue();
}
}

//for unitTests, as we cannot run the protected ExecuteAsync method directly
public async Task ExecuteTasksFromQueue()
{
while (!_tasks.IsEmpty)
{
var taskToRun = _tasks.Dequeue();
if (taskToRun == null) return;

//call the relevant service
var serviceToRun = _backgroundServiceableList
.FirstOrDefault(b => b.Name() == taskToRun.Name);

if (serviceToRun != null)
{
await serviceToRun.Execute(taskToRun);
}
}
}
}

The GenericBackgroundTaskService is similar to what I had in the previous article, but here it gets a list of implementations of IBackgroundServiceable (most DI can do this, it can be very useful).

Every few seconds we go over all of the tasks that we have in the queue, and instead of calling a concrete service like we did in the previous solution, we find the specific service that we need to call by looking at the service names.

DI registration

Finally, I created some extension methods to register everything in the DI:

 public static class ServicesCollectionExtensions
{
public static void RegisterBackgroundService(
this IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<TasksToRun, TasksToRun>();
serviceCollection.AddHostedService<GenericBackgroundTaskService>();
}

public static void RegisterBackgroundServiceable(
this IServiceCollection serviceCollection)
{
//find all implementations of this interface
var (attributeValidatorType, concreteTypes) = FindImplementations();

foreach (var type in concreteTypes)
{
serviceCollection.AddScoped(attributeValidatorType, type);
}
}

public static (Type attributeValidatorType, IEnumerable<Type> concreteTypes)
FindImplementations()
{
var attributeValidatorType = typeof(IBackgroundServiceable);
var concreteTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p =>
attributeValidatorType.IsAssignableFrom(p) && !p.IsInterface);

return (attributeValidatorType, concreteTypes);
}
}

I have created 2 separate methods:
One is to register the TasksToRun container and the GenericBackgroundTaskService background service. And a separate method to register all the implementations of IBackgroundServiceable so we will not need to register them by hand. I have split it into separate methods in case someone needs to register the implementations of IBackgroundServiceable using some customer logic.
I also have a service method that just finds all implementations to help anyone who needs to register them.

How it all comes together:

let's assume that we need to perform 2 actions in the background, monitoring and importing. We can create a constants file to keep the service names to avoid any spelling issues

public static class ServiceNames
{
public const string MonitorService = "monitoring";
public const string ImportService = "importing";
}

The monitoring and import service can look like this:

public class MonitorService : IBackgroundServiceable
{
public string Name() => ServiceNames.MonitorService;

public async Task Execute(BackgroundTaskSettings settings)
{
var monitorSettings = settings.Settings.ToObject<MonitorSettings>();
//monitor something
....
}
}

public class ImportService : IBackgroundServiceable
{
public string Name() => ServiceNames.ImportService;

public async Task Execute(BackgroundTaskSettings settings)
{
var importSettings = settings.Settings.ToObject<ImportService>();
//import something
....
}
}

Notice that we have to add a line to parse the settings JObject to a class.

Let's take a look at our main business service

public class BusinessService : IBusinessService
{
private readonly TasksToRun _tasksToRun;

public BusinessService(TasksToRun tasksToRun)
{
_tasksToRun = tasksToRun;
}

public async Task DoSomething(string val)
{
_tasksToRun.Enqueue(new BackgroundTaskSettings(
ServiceNames.MonitorService,
JObject.FromObject(new MonitorSettings
{
ApiKey = "AA-BB-CC11",
ExternalValue = val
})));

await Task.Delay(1000);
}
}

Here we get the TasksToRun container from the DI, and just enqueue a task for it to run. (I am also using ServiceNames.MonitorService to make sure that the service names are defined in a single place.)
TasksToRun.Enqueue
also has an overload that takes a list of BackgroundTaskSettings in case we need to do several things.

Unit-test:

With this new and improved solution, it is very easy to wire a unit test for our main business service

[TestClass]
public class BusinessServiceTests
{
[TestMethod]
public async Task DoSomethingTests()
{
var tasks = new TasksToRun();
var service = new BusinessService(tasks);

var expectedVal = "testing11";
await service.DoSomething(expectedVal);

var task = tasks.Dequeue();
Assert.IsNotNull(task);
Assert.IsTrue(
HasSettingsValue(task.Settings, "ExternalValue", expectedVal));
}
}

public static bool HasSettingsValue(JObject obj, string key, string val)
{
var objVal = obj[key];
return objVal?.ToString() == val;
}

Now that the background service is generic I have exported it into a Nuget that you can use.
You can see the full code here.

--

--