In this post we'll see how to create background tasks within our C# applications and what are some of the different options available.
The reason I've chosen these three libraries is because I've used all of them in a professional setting. And in my opinion, each of them has its own use case.
Table of Contents
As always, the code is on GitHub and for this particular post, I've created a basic web API project so we can see all the options together.
1 - Native Background Workers in C#
The first option is the “native” one, in my opinion, it’s a good choice when you need something running all the time, one after another.
For this, C# gives us a template called “Worker Service,” although you can obviously add it to any kind of project. In this case, we'll add a worker to a web API.
The first thing is to create that worker, the main thing here is that it inherits from the BackgroundService
class:
public class NativeWorker : BackgroundService
{
private readonly ISampleUseCase _sampleUseCase;
public NativeWorker(ISampleUseCase sampleUseCase)
{
_sampleUseCase = sampleUseCase;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_sampleUseCase.Execute();
await Task.Delay(1000, stoppingToken);
}
}
}
This code will run continuously once the application starts. When it finishes, it starts over again.
And you simply configure it in your startup as a HostedService
and it works:
builder.Services.AddHostedService<NativeWorker>();
This functionality is very useful when, for example, you need to do continuous polling of certain information, or really anything that needs to be running 24/7.
By the way, BackgroundServices are singletons, which means that anything injected through dependency injection must also be a singleton, and normally it won’t be. For that scenario, there’s a workaround where you inject the IServiceProvider
and create a scope with it, making it work correctly:
using (IServiceScope scope = _serviceProvider.CreateScope())
{
ISampleUseCase sampleUseCase = scope.ServiceProvider
.GetRequiredService<ISampleUseCase>();
sampleUseCase.Execute();
}
Now let’s move on to the alternative options, which are libraries ready for more scenarios. Of course, you could put a timer or some logic in the BackgroundService
so it runs once an hour, but there are other options for that, as I’ll show.
2 - Background Tasks in C# with HangFire
Within C#, two libraries are more popular than the rest. The first of these is HangFire. Personally, it’s my favorite and the one I have the most experience with, in fact, it’s the one I use on this blog to send notifications, newsletters, etc.
Installing HangFire is very straightforward: once you have the Hangfire.AspNetcore
package, you just need to indicate in your startup where you're going to "store" the information to be sent.
Let me explain: when I create a post, I send a notification on the web (the typical bell icon above) and an email to those who subscribe to the course that this post is part of (it can be configured so nothing is received). I do this with HangFire, using its Fire&Forget functionality.
For this kind of communication, you need to store what you want to send somewhere. In Hangfire, you can configure this to be in-memory, in Redis, in a database, etc. You install the package you need; in my case, Hangfire.MemoryStorage
, and you do it like this:
builder.Services.AddHangfire(config => config.UseMemoryStorage());
builder.Services.AddHangfireServer(); //this is needed because it's the "server"
Then you just need to invoke HangFire in your code and specify the method you want to run with its parameters, that’s it, it works:
[ApiController]
[Route("[controller]")]
public class HangFireController : ControllerBase
{
private readonly SendPostCreatedNotification _sendPostNotification;
public HangFireController(SendPostCreatedNotification sendPostNotification)
{
_sendPostNotification = sendPostNotification;
}
[HttpGet("hangifre-execution")]
public bool HangfireExecution()
{
BackgroundJob.Enqueue(() => _sendPostNotification.Execute("identifier-1"));
return true;
}
}
In this case, we trigger the job from within the controller using BackgroundJob
, then use Enqueue
to add it to the execution queue.
Of course, the code isn't going to wait for the job to finish before returning the controller result.
Obviously this is just one example of many: you could also schedule a job to run in X time, have it repeat, or send several at once (in the pro version).
If anyone is familiar with Ruby, HangFire is the equivalent of Sidekiq.
3 - Background Tasks in C# with Quarz.NET
The second option we'll look at now is Quarz.NET, similar to the previous one but a little different. Originally, this library is a port of a Java library.
Let's get to it: to use it, we obviously need to install the Quartz
library and the Quartz.Extensions.Hosting
package. And of course, we have to define it in the application's startup:
builder.Services.AddQuartz();
Previously you had to pass UseMicrosoftDependencyInjectionScopedJobFactory
in the configuration, but that's no longer needed since it's the default method now.
Then, for safety, you can add the following code, which makes the application wait until any running jobs are finished before shutting down. So, the startup configuration looks like this:
builder.Services.AddQuartz()
.AddQuartzHostedService(config=>config.WaitForJobsToComplete = true);
Now we'll create a job, which is basically the piece of code we're going to execute in the background. You can use dependency injection, by the way:
public class QuartzExampleJob : IJob
{
private readonly ISampleUseCase _sampleUseCase;
public QuartzExampleJob(ISampleUseCase sampleUseCase)
{
_sampleUseCase = sampleUseCase;
}
public Task Execute(IJobExecutionContext context)
{
_sampleUseCase.Execute();
return Task.CompletedTask;
}
}
As you can see, we have to use the IJob
interface, which means if you have the scenario I mentioned earlier about my blog, it doesn't fit exactly with what I want. But it’s a totally valid and even elegant alternative to using the built-in BackgroundService.
For that reason, I think where it really shines is for creating cron jobs. And what is a cron job? Basically, it's a task that runs every X minutes or at a certain time every day.
This functionality is very easy to configure, you can do it simply by specifying AddQuartz
:
builder.Services.AddQuartz(config =>
{
JobKey key = new JobKey("QuartzExampleJob");
config.AddJob<QuartzExampleJob>(jobConfig => jobConfig.WithIdentity(key));
config.AddTrigger(opts => opts
.ForJob(key)
.WithIdentity("QuartzExampleJob-trigger")
.WithCronSchedule("10 * * * * ?"));
})
.AddQuartzHostedService(config=>config.WaitForJobsToComplete = true);
You can see that we're creating a key that "links" our IJob to the Quartz configuration. And on the last line, we set it up as a cron job so it runs every 10 minutes.
Alternatively, you can put the configuration in appsettings and load it via a configuration file, or, for example, configure it to run once a day:
config.AddTrigger(opts => opts
.ForJob(key)
.WithIdentity("QuartzExampleJob-trigger")
.WithDailyTimeIntervalSchedule(daily =>
daily.WithIntervalInHours(24)
.OnEveryDay()
.StartingDailyAt(TimeOfDay.HourAndMinuteOfDay(10, 00))
));
As you can see, it requires a bit more configuration, so if you want something more complex you might want to check the official documentation.
Personally, I really like it as it's quite common to see this kind of job created in the Windows Task Scheduler, which is obviously a terrible solution.
If there is any problem you can add a comment bellow or contact me in the website's contact form