In a previous post, I discussed how we can use Configuration API in .net core console application to retrieve custom configuration settings from a Json
file, utilising a convenient feature of the Microsoft.Extensions.Configuration namespace.
In this post, I will provide an example of how to develop .NET Core console applications using IHost
, generic interface to develop headless services.
One of the most common uses for a Console Application is to build what is known as headless services, this is particularly common when working with Docker containers and Kubernetes (K8S).
Using headless services, it is possible to create a service grouping that does not allocate an IP address or forward traffic, enabling you to definitively control what specific pods you connect and communicate with.
What is .net core generic host
A common scenario or use case for headless services is to run in a Fire And Forget asynchronous pattern, which facilitate background processing tasks, perhaps handling messages on a queue for example, common in a cloud native, container based architectures.
The goal of the Generic Host is to decouple the HTTP pipeline from the Web Host API to enable a wider array of host scenarios. The Generic Host is new in ASP.NET Core 2.1 and isn't suitable for web hosting, but is ideally used for Console Applications which are typically used to develop headless services.
The Generic Host library is available in Microsoft.Extensions.Hosting
namespace and provided by Microsoft.Extensions.Hosting
package.
To add the namespace using the .net core CLI
dotnet add package Microsoft.Extensions.Hosting
Introducing IHost and the HostBuilder
In .NET Core 2.1 Generic Host enables developers easily set up cross-cutting concerns for non-web based applications including:
- Configuration
- Logging
- Dependency Injection (DI)
The IHost
interface offers a number of benefits and features:
- Graceful shut down
- Dependency Injection
- Logging
- Configuration
These features are particularly important when developing complex data processing tasks in Docker containers. Especially graceful shutdown, as it helps to keep application state consistent.
Implement IHost in .net core console applications
The IHost
interface and the HostBuilder
class provide similar experience to the respective IWebHost
and WebHostBuilder
.net core web developers are familiar with when using ASP.net core.
You need to bear in mind that applications which will implement the IHost
interface will typically be required to run asynchronously. This will usually require marking the Main
method as async
.
public static async Task Main(string[] args) { var host = new HostBuilder() .Build(); await host.RunAsync(); }
<PropertyGroup> <LangVersion>latest</LangVersion> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp2.2</TargetFramework> </PropertyGroup>
In order to continue developing our application we will need to add several more package references
dotnet add package Microsoft.Extensions.Configuration dotnet add package Microsoft.Extensions.Configuration.CommandLine dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables dotnet add package Microsoft.Extensions.Configuration.Json dotnet add package Microsoft.Extensions.Hosting dotnet add package Microsoft.Extensions.Hosting.Abstractions dotnet add package Microsoft.Extensions.Logging dotnet add package Microsoft.Extensions.Logging.Configuration dotnet add package Microsoft.Extensions.Logging.Console
HostBuilder does not provide an extension method enabling the use of a StartUp
class
The HostingHostBuilder provides extension methods to configure host services. The following extension available:
ConfigureAppConfiguration
– Application ConfigurationConfigureContainer
– Configure the instantiated dependency injection containerConfigureLogging
– Configure LoggingConfigureServices
– Adds services to the Dependency Injection containerRunConsoleAsync
– Enables console supportUseConsoleLifetime
– Listens for shutdown signalsUseContentRoot
– Specify the content root directoryUseEnvironment
– Specify the environment
The RunConsoleAsync
will start services and wait on an exit signal in the application.
Building a Host
Using the Main method create a HostBuilder and use extension methods to register services with DI, read configuration and configure the logging for your application.
Host Configuration
A host is defined as a container or machine that your application is going to run on. So the information that is stored here, is information that is pertinent to the environment. So for instance, this could be information that defines the Container as either a Production, Staging or Development environment.
This may be useful to your application because it may do different things based on which environment it is running in. For instance, it may actually load up a different AppSettings.json file based on the environment it is executing in. Similar to how ASP.net Core applications load up appsettings.Development.json
or the appsettings.Production.json.
In all likelihood this information may not actually be shipped with your application binaries and may only be available once the application start up. This information may be stored Environment Variables or the Host Settings file my be stored in a central location so several application may make use of it.
To configure the Host we will use ConfigureHostConfiguration
to initialize the IHostingEnvironment
for use later in the build process. Environment variable configuration isn't added by default,
In order to get the information from Environment Variables, we have to call AddEnvironmentVariables
on the host builder to configure the host from environment variables. AddEnvironmentVariables
accepts an optional user-defined prefix, by convention use your application name in our example we've declared a constant string value with _prefix
set with the application name.
Application Configuration
The application configuration is the area where you start wiring up all the components, dependencies and logging information you application is going to require. So far instance, depending on what environment your application is running in it may be connecting to a different database or going to respond to events from a different queue.
This is where you will load up a specific application settings file depending on the environment.
Use the ConfigureAppConfiguration
configuration providers to construct the final representation of configuration values for our application.
In the example below we read the configuration values from an appsettings.json file first, followed by environment variables and finally from any arguments passed into the application.
ConfigureServices
is where we configure the services we want our application to run, registering services with the ServiceCollection. Registration is performed using extension methods on the ServiceCollection and once complete, enabling DI in our application.
class Program { private const string _prefix = "FUZZBIZZ_"; private const string _appsettings = "appsettings.json"; private const string _hostsettings = "hostsettings.json"; public static async Task Main(string[] args) { var host = new HostBuilder() .ConfigureHostConfiguration(configHost => { configHost.SetBasePath(Directory.GetCurrentDirectory()); configHost.AddJsonFile(_hostsettings, optional: true); configHost.AddEnvironmentVariables(prefix: _prefix); configHost.AddCommandLine(args); }) .ConfigureAppConfiguration((hostContext, configApp) => { configApp.SetBasePath(Directory.GetCurrentDirectory()); configApp.AddJsonFile(_appsettings, optional: true); configApp.AddJsonFile( $"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true); configApp.AddEnvironmentVariables(prefix: _prefix); configApp.AddCommandLine(args); }) .ConfigureServices((hostContext, services) => { services.AddLogging(); services.Configure<Application>(hostContext.Configuration.GetSection("application")); services.AddHostedService<FizzBuzzHostedService>(); }) .ConfigureLogging((hostContext, configLogging) => { configLogging.AddConsole(); }) .UseConsoleLifetime() .Build(); await host.RunAsync(); } }
Using IHostedService
The actual implementation of the service is defined using IHostedService
interface. When an application is started, it will call StartAsync
method.
At shutdown, StopAsync
is called and the service cleans up a little before the application is killed.
IHostedService
implementation defined and registered with DI container using ConfigureServices
Usually in Linux daemons, your service implementation will be a Singleton
class - only one instance of the service for the application lifetime.
It also adds the configuration POCO (Plain Old C# Object) to the the services for dependency injection. This implements the IOption
interface in .NET Core, which can take a type that it will attempt to bind CLI parameters and environment variables to based on the field name.
using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; public class SampleService : IHostedService { public Task StartAsync(CancellationToken cancellationToken) { throw new System.NotImplementedException(); } public Task StopAsync(CancellationToken cancellationToken) { throw new System.NotImplementedException(); } }
Summary
Console application with one or more IHostedServices are ideal for developing Headless Services for data processing running in Docker containers. When the container is shut down, the processing can stop gracefully.
Dependency Injection gives you many options to test your code, enabling separation of concerns.
There are a number of concepts introduced with Generic Host, which are essential to simplify the development of microservices . A single common pattern for web applications as well as services, with easy access to things like DI, logging and configuration is extremely welcome.
Additional Resources
- What is this Directory.Packages.props file all about? - January 25, 2024
- How to add Tailwind CSS to Blazor website - November 20, 2023
- How to deploy a Blazor site to Netlify - November 17, 2023