.NET Core 3.0 worker app with a database, hosted on a Generic host

Eduard Los
4 min readJul 19, 2019
Photo by Afonso Lima from FreeImages

Not that long ago Microsoft introduced .NET Generic Host. A library which makes our life much more easier, when it comes to all kinds of message processing, background tasks, and other non-HTTP workloads. No more custom console applications with Console.ReadLine(); to keep it running in the background.

Ok! Enough talks! Let’s jump straight to the implementation.

We’ll need .NET Core 3 for this and then we can create our worker either from VS UI or just in our favourite terminal, using command:

dotnet new worker

Alright, we have our project created, so let’s briefly go through the code and see what’s in there for us.

The Program.cs file is the place where we’ll be bootstrapping our worker. From the start we have Microsoft’s dependency container, logging and configuration functionality out of the box. Let’s take a short look to the code and some of extension methods:

ConfigureAppConfiguration

Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, builder) =>
{
builder
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json")
.AddEnvironmentVariables()
.Build();
})

There’s not much to say about ConfigureAppConfiguration method — it’s the method which populates our configuration from different sources. You obviously don’t want to hardcode your secrets and connection strings, right?
:speak_no_evil: Also, it is worth mentioning that configuration variables with the same names are rewritten with each extension method call.

Quick tip: if we have variable “LogLevel”: “information” in appsettings.json, and “LogLevel”: “error” in environments variables, the end result populated in the configuration will be the last one.

ConfigureServices

.ConfigureServices((hostContext, services) =>
{
var telemetryClient = new TelemetryClient
{
InstrumentationKey = hostContext.Configuration.GetValue<string>("ApplicationInsights:InstrumentationKey")
};
services.AddSingleton(t => telemetryClient);

services.AddDbContext<DemoDbContext>(opts =>
opts.UseSqlServer(hostContext.Configuration.GetConnectionString("DemoDb")));
services.AddHttpClient<IDemoHttpClient, DemoHttpClient>(config =>
config.BaseAddress = new Uri(hostContext.Configuration.GetValue<string>("DemoBaseUrl")
))
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryForeverAsync(attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))));
services.AddHostedService<Worker>();
})

ConfigureServices, like with the WebHost, is the method where we add our services to the dependency container. I have added few most common ones here as an example. Let’s quickly move through.

First we have quite standard services — ApplicationInsights telemetry to see what’s going on there and database context registration. Then we register our HttpClient using HttpClientFactory which was introduced in .NET Core 2.1. This helps us to avoid a lot of problems with misusage of the HttpClient class and avoid ‘sockets exhaustion’ problem. See You’re using HttpClient wrong and it’s destabilizing your software blog post for more detailed information.

Another really cool part about using HttpClientFactory is Microsoft.Extensions.Http.Polly nuget package. This package integrates IHttpClientFactory with the Polly library, to add transient-fault-handling and resiliency through fluent policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback.

And the last line services.AddHostedService<Worker>(); is used to register our implementation of IHostedService interface.

Configure container

This is the method which is used to plug in the custom dependency container, which is really awesome. In this example i will use Lamar which is the successor to StructureMap library. One of the reasons why you might want to consider custom container is a bit more advanced dependencies configuration and management fro a bigger solution. One of my favourite pros is that you can split your configuration to registries, this is very useful, when the solution is big or consists of multiple projects. So if we look to the code, we have something like that:

.ConfigureServices((hostContext, services) =>
{
services.AddDbContext<DemoDbContext>(opts => opts.UseSqlServer(hostContext.Configuration.GetConnectionString("Dem oDb")));
})
.ConfigureContainer<ServiceRegistry>((ctx, container) => container.IncludeRegistry<WorkerRegistry>())
.UseLamar();

.UseLamar() extension method just uses UseServiceProviderFactory(IServiceProviderFactory<TContainerBuilder>) method and overrides the default factory used to create the app’s service provider as is it mentioned in the documentation from Microsoft. Looks a lot cleaner, right? Our Worker Registry will look something like that:

public class WorkerRegistry : ServiceRegistry
{
public WorkerRegistry()
{
For<TelemetryClient>().Use((ctx) =>
{
var configuration = ctx.GetInstance<IConfiguration>();
return new TelemetryClient { InstrumentationKey = configuration.GetValue<string>("ApplicationInsights:InstrumentationKey") };
}).Singleton();

this.AddHttpClient<IDemoHttpClient, DemoHttpClient>((ctx, config) =>
{
var configuration = ctx.GetService<IConfiguration>();
config.BaseAddress = new Uri(configuration.GetValue<string>("BaseUrl"));
})
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryForeverAsync(attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))));
this.AddHostedService<Worker>();

}
}

To use services with the scoped lifetime, we need to to create scope, because there’s no scope created for background service by default. Check the sample below:

public class Worker : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
...
...
using var scope = _serviceProvider.CreateScope();
var workerService = scope.ServiceProvider
.GetRequiredService<IWorkerService>();

workerService.DoSomeWorkWithDbAsync();
}

In my case Worker is the class where all the processing is started and from there we just follow all the best practices, like we normally do (right?
:smirk:) to keep it S.O.L.I.D.

ConfigureLogging

.ConfigureLogging((hostContext, builder) =>
{
builder.AddConsole();
builder.AddDebug();
builder.AddApplicationInsights(hostContext.Configuration.GetValue<string>("ApplicationInsights:InstrumentationKey"));

builder.AddFilter<ApplicationInsightsLoggerProvider>
(typeof(Program).FullName, LogLevel.Trace);
});

As you may see from the method name ConfigureLogging is the place where we configure logging for our worker. As you see i’ve added ApplicationInsights filter to be able to see all the logs in Application Insights.

UPD

If you want to run your app as Windows Service, you can use the Microsoft.Extensions.Hosting.WindowsService package and tell your new Worker that its lifetime is based on ServiceBase by using .UseWindowsService() extension method.

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

Very similar story is for Linux worker — Microsoft made integration with systemd, just add the Microsoft.Extensions.Hosting.Systemd and use .UseSystemd()extension method to hand the lifetime management to systemmd:

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

GL & HF!

--

--