Modern minimal workers in .NET
Tags: dotnet, csharp, backend, worker
Published: 2023-05-03 22:21:39

Since the release of .NET 6, we've heard a lot about [ASP.NET Core minimal APIs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis).
They've been discussed at Microsoft conferences, in blog posts, in YouTube videos, and on social networks.
We've all seen this kind of code sample of a minimal API:

```csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");
app.Run();
```

This is significantly more concise than controller-based web APIs, and we can all agree on that.
But did you know that **since the release of .NET 7**, there is also a way to **create minimal workers**?

A minimal worker console application looks like this:

```csharp
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<MyWorkerService>();

var app = builder.Build();
app.Run();
```

## Creating a minimal worker in .NET

Create an empty console application and reference the `Microsoft.Extensions.Hosting` package, at least [version 7.0.0](https://www.nuget.org/packages/Microsoft.Extensions.Hosting/7.0.0) (`Host.CreateApplicationBuilder()` has been added in this version).

You can also reference the `Microsoft.NET.Sdk.Worker` SDK in your project file, which is actually a dependency of the `Microsoft.NET.Sdk.Web` SDK, as [mentioned in the project's README](https://github.com/dotnet/sdk/tree/v7.0.203/src/WebSdk).
Referencing the `Microsoft.NET.Sdk.Worker` provides some benefits such as:

- `*.json` (like `appsettings.json`) files are automatically copied to the output and publish directories.
- A few `Microsoft.Extensions.*` [using directives](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-directive) are automatically added only if you enable implicit usings.

So your project file should look like this:

```xml
<Project Sdk="Microsoft.NET.Sdk.Worker">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0"/> <!-- or later -->
  </ItemGroup>
</Project>
```

You can even target .NET Framework:

```xml
<Project Sdk="Microsoft.NET.Sdk.Worker">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net462</TargetFramework>
    <LangVersion>10</LangVersion> <!-- required for top-level statements on .NET Framework -->
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0"/> <!-- or later -->
  </ItemGroup>
</Project>
```

`Host.CreateApplicationBuilder()` returns a`HostApplicationBuilder`, which shares some similarities with the `WebApplicationBuilder` used in ASP.NET Core minimal APIs.
You are probably already familiar with these properties:

- `HostApplicationBuilder.Services` to [register services](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection),
- `HostApplicationBuilder.Configuration` to add more [configuration providers](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration),
- `HostApplicationBuilder.Logging` to modify the [logging configuration and register more logging providers](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging),
- `HostApplicationBuilder.Environment` to have access to the [current environment](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments).

Here’s an example where we use all of these properties:

```csharp
var builder = Host.CreateApplicationBuilder(args);

if (!builder.Environment.IsDevelopment())
{
    builder.Configuration.AddAzureKeyVault(new Uri("https://mykeyvault"), new DefaultAzureCredential());
}

builder.Logging.AddSystemdConsole();

builder.Services.AddHangfire(/* [...] */);
builder.Services.AddHangfireServer();

/* [...] */

builder.Build().Run();
```

Next, register your background service(s) using `builder.Services.AddHostedService<YourConcreteBackgroundServiceType>()`.
The background service type must implement [IHostedService](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) or be a subclass of [BackgroundService](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice).
You can [learn more about background services here](https://learn.microsoft.com/en-us/dotnet/core/extensions/workers).

Keep in mind that if you host **long-running services** in your worker, you should consider **enabling server garbage collection** for performance implications.
You can learn more about this topic on this [documentation page about server garbage collection](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/workstation-server-gc#server-gc).

```xml
<PropertyGroup>
  <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
```

Let's wrap this up with a **final tip**.
If you want to create a **short-lived console application** that leverages the **full potential of minimal workers and .NET extensions libraries**, you can create a `BackgroundService` that calls `IHostApplicationLifetime.StopApplication()` when the work is done:

```csharp
internal sealed class MyShortLivedService : BackgroundService
{
    private readonly IHostApplicationLifetime _applicationLifetime;

    public MyShortLivedService(IHostApplicationLifetime applicationLifetime)
    {
        this._applicationLifetime = applicationLifetime;
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // [...]
        // Do some work then shut down the application
        this._applicationLifetime.StopApplication();
        return Task.CompletedTask;
    }
}
```

## References

- [Worker Services in .NET](https://learn.microsoft.com/en-us/dotnet/core/extensions/workers)
- [.NET extensions to the runtime libraries](https://learn.microsoft.com/en-us/dotnet/standard/runtime-libraries-overview)
- [.NET SDK GitHub repository](https://github.com/dotnet/sdk)