How to create a timely running service in .NET 7

Iqan Shaikh
4 min readSep 29, 2023

--

This article is an update to the previous article “how-to-create-a-timely-running-service-in-net-core”. In the update, dotnet 7 is used. Dotnet projects and Dockerfile are updated to use dotnet 7. Database Dockerfile is updated to use newer MS SQL server for linux.

Create a new service with generic host

Here we will use console app project only. So first create a new dotnet console application project.

dotnet new console --name MyBackgroundService

You will get a csproj file and Program.cs.

We will write code for building a generic host and run it async forever. This method will not let container die and backend processes can do their work.

using DummyService.App.Application.Handlers;
using DummyService.App.Application.Messaging;
using DummyService.App.Application.Storage;
using DummyService.App.Infrastructure.Messaging;
using DummyService.App.Infrastructure.Storage;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.IO;
using System.Threading.Tasks;

namespace DummyService.App
{
class Program
{
public static async Task Main(string[] args)
{
var hostBuilder = new HostBuilder()
.ConfigureAppConfiguration((hostContext, configBuilder) =>
{
configBuilder.SetBasePath(Directory.GetCurrentDirectory());
configBuilder.AddJsonFile("appsettings.json", optional: true);
configBuilder.AddJsonFile(
$"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json",
optional: true);
configBuilder.AddEnvironmentVariables();
})
.ConfigureLogging((hostContext, configLogging) =>
{
configLogging.AddConfiguration(hostContext.Configuration.GetSection("Logging"));
configLogging.AddConsole();
})
.ConfigureServices((hostContext, services) =>
{
services.AddSingleton<IEndpointConfiguration>(serviceProvider =>
{
return hostContext.Configuration.GetSection("EndpointConfiguration").Get<EndpointConfiguration>();
});

services.AddDbContext<DummyDbContext>(options =>
{
options.UseSqlServer(hostContext.Configuration.GetConnectionString("DummyDatabase"));
});

services.AddScoped<IMessageDatabase, SqlMessageDatabase>();

services.AddScoped<IDummyEventHandler, DummyEventHandler>();

services.AddScoped<IMessageProcessor, MessageProcessor>();

services.AddHostedService<TimedHostedService>();
});

await hostBuilder.RunConsoleAsync();
}
}
}

This piece of code will create a generic host, add some basic features like configuration and logging. Please remember to pull required packages from nuget related to logging, configuration, dependency injection,etc. I have struggled a lot resolving dependencies because they all are in separate nuget packages.

  • Microsoft.Extensions.Configuration: Is used to read configurations from different sources like JSON files, environment variables, etc. For sources you will need separate nuget package. For example, to read from JSON file you will need to add Microsoft.Extensions.Configuration.Json package.
  • Microsoft.Extensions.DependencyInjection: Provides basic functionalities for dependency injection. It is used to register dependencies and life cycles here.
  • Microsoft.Extensions.Hosting: Is used to build a generic host and run it forever.
  • Microsoft.Extensions.Logging: Is used to add logging in app. We are logging in console for this project.

First part is about adding configuration in host. We are using appSettings.json file and environment variables in this app. It goes by precedence. Environment variables will override any configuration here. We will be assigning some environment variables with while running docker containers.

Secondly, we add logging. Logging configuration like log file path, level, etc. are defined in configuration file. We will be writing logs in console for this app.

Services are nothing but internal project dependencies. You can add database context, worker and endpoint dependencies. We will update this section when we have our interfaces and implementations ready for this service.

Setup timed worker

A timed worker or host is a simple background service which runs certain tasks at a defined interval of time. We will run service continuously, but If you want to add certain time gap between processes, you can add in here. So create a new class, same as below.

using DummyService.App.Application.Messaging;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;

namespace DummyService.App
{
public class TimedHostedService : BackgroundService
{
private readonly ILogger _logger;
private readonly IMessageProcessor _messageProcessor;

public TimedHostedService(ILogger<TimedHostedService> logger, IMessageProcessor messageProcessor)
{
_logger = logger;
_messageProcessor = messageProcessor;
}

public override Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Service is starting.");
return _messageProcessor.StartProcessingAsync();
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Service is running.");
await Task.Delay(1000, stoppingToken);
}
}

public override Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Service is stopping.");
return _messageProcessor.StopProcessingAsync();
}
}
}

This TimedHostedService will inherit from BackgroundService instead of IHostedService mentioned in previous article. We can still use IHostedService and implement it but it’s easier with Abstract class BackgroundService. We are adding logging dependency through constructor injection here.

StartAsync:

This method will be called when we run the host from Main method. It starts Message Processor.

StopAsync:

This method is called when the host is shut down. This method stops the Message Processor. We can also add some features like disposing message processor, complete in-progress transactions and shut down gracefully.

ExecuteAsync:

This method does all work that we intend our service to do. We write code for actual process here, and it will be run at every interval of time. We have already started the Message Processor to receive message, so here it will just log something in console every second.

Now, let’s register dependency in Program.cs file.

services.AddHostedService<TimedHostedService>();

This statement will register dependency of IHostedService. Now the host is ready. You can run the app and see that console will show up and it will log some message every five seconds.

That’s it. You have your service setup. Now you can add more features in it or do anything you like in DoWork method.

If you need code for reference, checkout the repository.

--

--

Iqan Shaikh

Sr. Full Stack Engineer | C# .NET | Azure | AWS | DevOps | React | Node | Docker | Flutter | ReactNative