Introduction – What is a Worker Service?
Background Services or Worker Services are scheduled programs which do a certain functionality, iteratively over a scheduled period of time. These services run on the same web server along with any web application or API and can also share libraries and components with one another.
Unlike a WebApp or an API which depend on user interaction, these services run independent of user interaction and can be configured to start once the web application is deployed and killed when the application is down.
One peculiar usecase is a service which runs in parallel to a blog application, that sends PostNotifications to users who are subscribed in the blog application.
Background Services in ASP.NET Core
In ASP.NET Core, we can implement such a service which runs similar to a console application by implementing the IHostedService interface or by simply overriding required methods in the BackgroundService abstract class. We can also extend the use of Dependency Injection capabilities within these Background Services by means of the IServiceProvider interface.
In this article, let’s look at how we do all these, by building a simple PostNotificationService that picks up the subscribed user email addresses and sends mails to them with a predefined content for a preconfigured schedule.
Getting Started
As mentioned before, to better understand how BackgroundServices work in ASP.NET Core we’ll build our own service called PostNotificationService that pulls email addresses from the DataStore and sends predefined Mails to every email address. Although we have tons of services for this usecase, it doesn’t really hurt to actually create one by ourselves right?
To get started, we’ll first create our .NET Core project which hosts our service. The project type for this is different from your typical ASP.NET Core project types. In this case, the csproj file looks as below:
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.3" />
</ItemGroup>
</Project>
Observe that we have an explicit reference to Microsoft.Extensions.Hosting package, which is generally not present in any other projectType such as a WebApp (which is of type Microsoft.NET.Sdk.Web). This is because in case of the WebApps this package is referenced implicity by the framework whereas for a Worker type its not the case.
Next, let’s create a class PostNotificationService which is our worker service. To do this, we have to options:
- Implement the IHostedService interface and do the stuff yourself
- Extend the BackgroundService class and override whatever is required for you
Both the IHostedService and BackgroundService classes are a part of the Microsoft.Extensions.Hosting package
The IHostedService interface looks like below:
public class IHostedService {
Task StartAsync (CancellationToken cancellationToken);
Task StopAsync (CancellationToken cancellationToken);
}
As the names suggest, the StartAsync() method is invoked by the host when the worker is ready to be executed and the StopAsync() method is invoked when the host is shutting down the service gracefully. In both the cases we also pass a CancellationToken for cases when the operation is no more graceful but a forceful execution. The StartAsync() method generally contains the logic that the service is meant to execute.
To create our own service, we can implement this interface and handle the Task management by ourselves. It can look like below:
public class PostNotificationService : IHostedService
{
private Task _executingTask;
private CancellationTokenSource _cts;
public Task StartAsync(CancellationToken cancellationToken)
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// My PostNotificationService logic spans here
return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
if (_executingTask == null)
{
return;
}
_cts.Cancel();
await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));
cancellationToken.ThrowIfCancellationRequested();
}
}
Another approach to this is, by using the BackgroundService abstract class which has all these methods implemented for you and exposes an abstract method ExecuteAsync() where your service functionality can reside.
public abstract class BackgroundService: IDisposable, IHostedService {
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
}
Our PostNotificationService can extend this BackgroundService class and override the ExecuteAsync() method with the functionality. The ExecuteAsync() method is called when the Host starts the service (i.e when the StartAsync() from the IHostedService implementation is called).
registering a Hosted “Service”
We have been maintaining all along that this service is invoked by the Host, but how do we configure this service to be invoked by the host? The answer is when we’re building the Host in the Main() method. We can configure this as a HostedService, while configuring other services in the Host service pipeline.
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder
.UseContentRoot(Directory.GetCurrentDirectory())
.UseKestrel()
.UseIISIntegration()
.UseStartup<Startup>()
.UseSetting("detailedErrors", "true")
.ConfigureServices(services =>
{
services.AddHostedService<PostNotificationService>();
})
.CaptureStartupErrors(true);
});
What if we don’t configure the PostNotificationService after the CreateDefaultBuilder() method?
The default behaviour of any implementation of IHostedService is that the StartAsync() is invoked even before the application pipeline is built. This would be a bummer for us if we need to inject any services which are available in the application pipeline inside our hosted service. So we configure the hosted service to start only after the service pipeline is built and application is started.
Dependency Injection with Hosted Service
Speaking of Service pipeline, we can inject and use the services available in the DI within our HostedService by means of the IServiceProvider interface which can be injected via constructor.
public class PostNotificationService : BackgroundService
{
private readonly IServiceProvider _sp;
public PostNotificationService(
IServiceProvider sp)
{
_sp = sp;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
await SendPostNotifications(cancellationToken);
await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken);
}
}
private async Task SendPostNotifications(CancellationToken cancellationToken)
{
using (var scope = _sp.CreateScope())
{
IMailSubscriptionService repo = scope.ServiceProvider.GetRequiredService<IMailSubscriptionService>();
IEmailManager em = scope.ServiceProvider.GetRequiredService<IEmailManager>();
IConfigurationManager conf = scope.ServiceProvider.GetRequiredService<IConfigurationManager>();
var view = await repo.GetDigestEmailRenderedView();
foreach (var subscriber in repo.GetSubscribersForDigestEmails())
{
await Task.Run(() =>
{
em.SendMail(
subscriber.EmailAddress,
conf.ContributeMailContentConfiguration.Subject,
view);
});
}
}
}
}
The dependencies IMailSubscriptionService, IEmailManager and IConfigurationManager encapsulate respective functionalities such as fetching emailAddress from the DataStore, rendering the information in a preconfigured template and sending Emails via SMTP.
How is this service scheduled?
The below code snippet inside the ExecuteAsync() does the trick.
while (!cancellationToken.IsCancellationRequested)
{
await SendPostNotifications(cancellationToken);
await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken);
}
It repeats the function call to SendPostNotifications() method that holds the business logic of the service, while placing a five minute delay between every consecutive function call.
This loop repeats untill the web application is stopped, which implicity calls for the cancellationToken via the StopAsync() method of the implemented IHostedService interface.
Conclusion
We have so far seen with an example, about how we can create a simple worker process which runs inside our webserver alongside our ASP.NET Core web application in the background and how we can levarage the power of DI inside these worker processes using the IServiceProvider.
We can further this implementation for complex use cases such as processing Message Queues and so on, but at the basic level, this is how it is done.
Found this article helpful? Please consider supporting!