NET Core Background Services_870_220

.NET Core 2.1 introduced the Generic Host, which allows you to launch an app similar to that of ASP.NET Core but for processing non-HTTP requests.

Meaning, the generic host takes all the goodness that ASP.NET Core provides for cross-cutting concerns, such as its built-in dependency injection, logging and configuration, and allows you to build on top of that for non-HTTP scenarios.

The most typical scenario is for a worker service or any type of long-running process. This could be a service that does some work and then sleeps for a period of time before doing more work. An example of this would be a polling service to fetch data from an external web service. Another really common use case would be a service that pulls messages of a queue and processes them.

.NET Core 3 Worker Service Template

With .NET Core 3, there is a new template available that uses the Generic Host and gives you the basic scaffolding of creating a worker service.

If you're using the CLI, you can generate a new service worker easily:

dotnet new worker MyWorkerServiceApp

Microsoft.Extensions.Hosting

The Generic Host library is the Microsoft.Extensions.Hosting NuGet package.

If you open up the .csproj file generated by the template, you will see the package being referenced as well as the Microsoft.NET.Sdk.Worker being referenced:

<Project Sdk="Microsoft.NET.Sdk.Worker">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" />
  </ItemGroup>
</Project>

Program.cs should look familiar if you've worked with ASP.NET Core:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

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

    public static IHostBuilder CreateHostBuilder(string[] args) =>
      Host.CreateDefaultBuilder(args)
          .ConfigureServices((hostContext, services) =>
          {
            services.AddHostedService<Worker>();
          });
  }
}

You can configure various services just like you would in ASP.NET Core; Dependency Injection via AddSingleton() or AddTransient(); logging via AddLogging(); or configuration via AddOptions().

The main difference is in ConfigureServices(), where the new extension method, AddHostedService<T> where T : class, IHostedService is called. This comes from the Microsoft.Extensions.Hosting.Abstractions package and is a transitive dependency from Microsoft.Extensions.Hosting.

The new class that was created from the template is called Worker and is used as the type parameter in AddHostedServices.

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

namespace WorkerServiceDemo
{
  public class Worker : BackgroundService
  {
    readonly ILogger<Worker> _logger;

    public Worker(ILogger<Worker> logger)
    {
      _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
      while (!stoppingToken.IsCancellationRequested)
      {
        _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
        await Task.Delay(1000, stoppingToken);
      }
    }
  }
}

The first thing to note is our class extends the abstract class, BackgroundService. This is a new class provided in .NET Core 3. It implements IHostedService, which is required by AddHostedService<T>.

Before we jump into BackgroundService, let's first take a look at IHostedService and what has to get implemented, if you were creating your own implementation.

using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Extensions.Hosting
{
  /// <summary>
  /// Defines methods for objects that are managed by the host.
  /// </summary>
  public interface IHostedService
  {
    /// <summary>
    /// Triggered when the application host is ready to start the service.
    /// </summary>
    /// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
    Task StartAsync(CancellationToken cancellationToken);

    /// <summary>
    /// Triggered when the application host is performing a graceful shutdown.
    /// </summary>
    /// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
    Task StopAsync(CancellationToken cancellationToken);
  }
}

You simply need to implement the StartAsync() and StopAsync() methods using the CancellationToken for graceful shutdown of your service.

You can probably already start to imagine how having an abstract class would be helpful, since the implementation would likely be similar for most scenarios where you want to create a long-running service.

BackgroundService

BackgroundService is new to .NET Core 3 and provides a simple abstract class for implementing a long-running service.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Extensions.Hosting
{
  /// <summary>
  /// Base class for implementing a long running Microsoft.Extensions.Hosting.IHostedService.
  /// </summary> 
  public abstract class BackgroundService : IHostedService, IDisposable
  {
    /// <summary>
    /// This method is called when the Microsoft.Extensions.Hosting.IHostedService starts. The implementation should return a task that represents
    /// the lifetime of the long running operation(s) being performed.
    /// </summary>
    /// <param name="stoppingToken">Triggered when Microsoft.Extensions.Hosting.IHostedService.StopAsync(System.Threading.CancellationToken) is called.</param>
    /// <returns>A <see cref="T:System.Threading.Tasks.Task" /> that represents the long running operations.</returns>
    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
  }
}

You simply need to implement Task ExecuteAsync(CancellationToken stoppingToken) while handling the CancellationToken that is used to determine when to stop your method.

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

namespace WorkerServiceDemo
{
  public class Worker : BackgroundService
  {
    readonly ILogger<Worker> _logger;

    public Worker(ILogger<Worker> logger)
    {
      _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
      while (!stoppingToken.IsCancellationRequested)
      {
        _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
        await Task.Delay(1000, stoppingToken);
      }
    }
  }
}

You'll notice the constructor takes an ILogger<Worker> as a dependency, which is resolved by the built-in dependency injection from the Generic Host. As mentioned, we can define other services to register with DI in the ConfigureServices() method in Program.cs.

Exchange Rate Polling Service

For a simple example, I'm going to create a service that is going to make an HTTP call every hour to an exchange rate web service to get the latest USD to CAD exchange rate.

Packages

I'm adding the latest version of Microsoft.Extensions.Http that provides us the ability to register the IHttpClientFactory and register the Newtonsoft.Json for deserializing the JSON response from the web service.

<Project Sdk="Microsoft.NET.Sdk.Worker">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="3.0.0-preview6.19304.6" />
    <PackageReference Include="Microsoft.Extensions.Http" Version="3.0.0-preview6.19304.6" />
    <PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
  </ItemGroup>
</Project>

Configure Services

As mentioned, I'm calling AddHttpClient() to register the IHttpClientFactory which I can inject into my worker class:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

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

    public static IHostBuilder CreateHostBuilder(string[] args) =>
      Host.CreateDefaultBuilder(args)
          .ConfigureServices((hostContext, services) =>
          {
            services.AddHttpClient();
            services.AddHostedService<Worker>();
          });
  }
}

Worker

The first thing is the IHttpClientFactory so we can get a new instance of the HttpClient. If you're unfamiliar with the IHttpClientFactory, check out the docs.

In the ExecuteAsync, I'm going to make an HTTP request to the api.exchangeratesapi.io service and get the latest exchange rate from USD to CAD. I'll handle relevant failures and use the logger to output. Likely here I'd be storing this data and persisting it somewhere for my application to use.

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace WorkerServiceDemo
{
  public class Worker : BackgroundService
  {
    private const string Symbol = "CAD";
    private const int ThreadDelay = 5000;

    private readonly ILogger<Worker> _logger;
    private readonly HttpClient _httpClient;
    private readonly JsonSerializer _serializer;

    public Worker(ILogger<Worker> logger, IHttpClientFactory httpClient)
    {
      _logger = logger;
      _httpClient = httpClient.CreateClient();
      _serializer = new JsonSerializer();
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
      while (!stoppingToken.IsCancellationRequested)
      {
        _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);

        try
        {
          var response = await _httpClient.GetAsync($"https://api.exchangeratesapi.io/latest?base=USD&symbols={Symbol}", stoppingToken);
          if (response.IsSuccessStatusCode == false)
          {
            _logger.LogCritical("Exchange Rate API Failed with HTTP Status Code {statusCode} at: {time}", response.StatusCode, DateTimeOffset.Now);
            continue;
          }

          using var sr = new StreamReader(await response.Content.ReadAsStreamAsync());
          using var jsonTextReader = new JsonTextReader(sr);
          var exchangeRateResult = _serializer.Deserialize<CurrencyExchange>(jsonTextReader);

          if (exchangeRateResult.Rates.TryGetValue(Symbol, out var cadValue))
          {
            _logger.LogInformation($"{Symbol} = {cadValue}");
          }
          else
          {
            _logger.LogCritical($"CAD Exchange rate not returned from API.");
          }
        }
        catch (HttpRequestException ex)
        {
          _logger.LogCritical($"{nameof(HttpRequestException)}: {ex.Message}");
        }

        await Task.Delay(ThreadDelay, stoppingToken);
      }
    }
  }

  public class CurrencyExchange
  {
    public string Base { get; set; }
    public DateTime Date { get; set; }
    public Dictionary<string, decimal> Rates { get; set; }
  }
}

Windows Service

If you're running .NET Core in Windows, you can install this worker service as a Windows Service.

Add the Microsoft.Extensions.Hosting.WindowsServices package to your .csproj file as a PackageReference

<PackageReference
  Include="Microsoft.Extensions.Hosting.WindowsServices"
  Version="3.0.0-preview6.19304.6" />

This adds an extension method called UseWindowsService() to the IHostBuilder.

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .UseWindowsService()
                .ConfigureServices((hostContext, services) =>
                {
                  services.AddHttpClient();
                  services.AddHostedService<Worker>();
                });

This allows you to run the application still as a console application or debug as you normally would through the CLI, Visual Studio, Visual Studio Code, Rider, etc. However, it also provides the ability to install (and then run it as a windows service).

cs create WorkerServiceDemo binPath=C:\Path\To\WorkerServiceDemo.exe

Summary

The Generic Host and the new BackgroundService in .NET Core 3 provides a convenient way to create long-running processes in .NET. You get all the wonderful features of dependency injection, logging, and configuration that you're used to in ASP.NET Core now for running long-running jobs or services.


DerekComartin
About the Author

Derek Comartin

Derek Comartin is software developer and Microsoft MVP with two decades of professional experience that span enterprise, professional services and product development. He’s written software for a variety of business domains, such as consumer goods, distribution, transportation, manufacturing, and accounting. He founded and leads the Windsor-Essex .NET Developers Group (@WENetDevelopers). Derek has a very active blog, codeopinion.com, that focuses on .NET, CQRS, Event Sourcing, HTTP APIs and Hypermedia.

Related Posts

Comments

Comments are disabled in preview mode.