Asynchronous programming
.NET Core Asynchronous C# Hosted Services Threading Visual Studio

Using Background Tasks with Hosted Services in .NET Core

Welcome to today’s post.

I will discuss how to create background tasks as hosted services within a .NET Core application.

A background service can be hosted in the following container application types:

  • A console application.
  • A windows forms application.
  • A web application.
  • A web API application.
  • A server scheduled task application.
  • A windows service application.
  • An Azure Web Job under an Application Service (web application).

In what scenarios can we be expected to use background services?  There are many situations where we want to run a process in the background asynchronously and only be concerned with the result. These can include:

  • Creating a user account in a web application.
  • Processing order during checkout.
  • Scheduled database clean-ups.
  • Periodically archiving transactional data.
  • Disable or activate customer accounts.

Start a workflow to kick off a complex orchestration.

In this post I will show how to create a console application that can run a background service.

A basic architecture is shown below utilizing a timer service to trigger an API call within the background hosted service:

Background tasks

I will show how to create a console application using .NET Core and run a background task running as a hosted service. The console application can be run as a scheduled task on a server or as a scheduled or triggered Azure web job.

I will show how to create a console application using .NET Core and run a background task running as a hosted service.

A console application can be created as a .NET Core application as shown:

Once created, the base application skeleton can be used as a basis for a console application that runs in the background to execute tasks in a background thread.

Later one, the logic can be shifted into a windows service or Azure web job

The decision to run the task as a long-running process or a short-running process will be determined by factors that can include:

  • Long duration, low frequency the task – maybe as a background thread task
  • Short job, higher frequency task – maybe as a scheduled task, job or serverless function.
  • The availability of computing power and cost.
  • Hosting costs.

The easiest host to run and debug is a console app. After our logic is tested thoroughly we can upgrade to one of the other hosted environments.  

The most commonly used background task is a timed background task that runs periodically and executes a task. A background task is implemented from an IHostedService interface which as two methods

public Task StartAsync(CancellationToken cancellationToken)

public Task StopAsync(CancellationToken cancellationToken)

Note: Once the IHostedService is registered and the application run you do NOT need to run a loop (while, for etc.) within the StartAsync() method .. this will hang the thread!

When the task class is registered to the dependency injection configured services collection within ConfigureServices() and run, the task hosted service methods will run after the application initial setup completes. The StartAsync() method is executed to initiate the task. When the task ends the StopAsync() method is executed.

Registering the task for dependency injection is done as follows:

services.AddHostedService<TimedHostedService>();

The implementation of the hosted service is shown below:

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using BookLoan.Services;
using BookLoan.Models;

namespace BackgroundTasksSample.Services
{
    internal class TimedHostedService : IHostedService, IDisposable
    {
        private readonly ILogger _logger;
        private Timer _timer;
        private readonly IBookService _bookService;

        public TimedHostedService(ILogger<TimedHostedService> logger,
            IBookService bookService)
        {
            _logger = logger;
            _bookService = bookService;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Timed Background Service is starting.");

            _timer = new Timer(DoWork, null, TimeSpan.Zero, 
                TimeSpan.FromSeconds(60));

            return Task.CompletedTask;
        }

        private void DoWork(object state)
        {
            _logger.LogInformation("Timed Background Service is working.");

            List<BookViewModel> list =                
              _bookService.GetChangedBooksList()
              .GetAwaiter()
              .GetResult();

            int numberBooks = list.ToArray().Length;

            _logger.LogInformation(String.Format(
              "Number of new books {0}", numberBooks));
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation(
              "Timed Background Service is stopping.");

            _timer?.Change(Timeout.Infinite, 0);

            return Task.CompletedTask;
        }

        public void Dispose()
        {
            _timer?.Dispose();
        }
    }
    #endregion
}

In the timer service we call the DoWork() method every 60 seconds and call a service method which executes a task (run a query to return a result, update data etc.)

The console application main method is shown below:

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using System.IO;

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Net.Http;

using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;

using BookLoan.Services;
using BookLoan.Helpers;
using BookLoan.Interfaces;
using BackgroundTasksSample.Services;

namespace BookLoanSignalRHubConsole
{
  public class Program
  {
    static void Main(string[] args)
    {
      var builder = new HostBuilder()
        .ConfigureAppConfiguration(
          (hostingContext, config) =>
          { 
            config.SetBasePath(Directory.GetCurrentDirectory());
            config.AddJsonFile("appsettings.json", true);
          })
          .ConfigureServices((hostingContext, services) =>
          {
            services.AddHttpClient();
            services.AddTransient<ApiServiceHelper, 
              ApiServiceHelper>();
            services.AddTransient<IApiServiceRetry, 
              ApiServiceRetry>();                                
            services.AddTransient<IApiServiceRetryWithDelay,  
              ApiServiceRetryWithDelay>();
            services.AddSingleton<IHttpContextAccessor, 
              HttpContextAccessor>();
            services.AddRouting();
            services.AddTransient<IBookService, BookService>();
            services.AddHostedService<TimedHostedService>();
          })
          .ConfigureLogging((hostingContext, logging) =>
          {
            logging.AddConfiguration(hostingContext.Configuration);
            logging.AddConsole();
            logging.AddDebug();
          });

          builder.UseConsoleLifetime();
          builder.Start();
        }
    }
}

To run the console in a loop we start the application hosted service using the UseConsoleLifetime() and Start() extension methods.

After running, the console should show with output informational and debug as shown:

This link details some more useful help on Hosted Services in .NET Core:

In future posts I will explore how to use background tasks within Windows Services in .NET Core.

That’s all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial