top of page

Introduction to Worker Services in ASP.NET Core

What are Worker Services ?

A worker service is a .NET project built using a template which supplies a few useful features that turn a regular console application into something more powerful. A worker service runs on top of the concept of a host, which maintains the lifetime of the application. The host also makes available some familiar features, such as dependency injection, logging and configuration.


Worker services will generally be long-running services, performing some regularly occurring workload.


Example of Worker Services:
  • Processing messages/events from a queue, service bus or event stream

  • Reacting to file changes in an object/file store

  • Aggregating data from a data store

  • Enriching data in data ingestion pipelines

  • Formatting and cleansing of AI/ML datasets


Working with Worker Service in ASP.NET ?

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:

  1. Implement the IHostedService interface and do the stuff yourself

  2. Extend the BackgroundService class and override whatever is required for you


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.



Source: referbruv.com


The Tech Platform

www.thetechplatform.com

0 comments
bottom of page