Introduction
In this article, I'll give an example about how to implement more advanced console applications using the package SimpleSoft.Hosting. The idea is to make it easier to setup dependency injection, logging and importing application settings by making use of the recent Microsoft.Extensions.*
packages but without too much boilerplate code. The concepts are similar to the package Microsoft.AspNetCore.Hosting
but without the unnecessary ASP.NET dependencies and should be useful when creating Windows Services, utility tools, self hosting WCF applications, or other needs you may have.
In this case, the example is a simple console application which does some periodic web requests. The addresses and batch delay are read from the application settings, NLog is used to log into files, and AutoFac is the dependency injection container.
Using the Code
The application is mainly distributed into three classes:
Program
- Entry point for the console application
HttpMonitorStartup
- Configuration class that registers and configures all the required dependencies
HttpMonitorHost
- The host entry point class
Program
Since this is a console application, I'll start by explaining the entry point class:
public class Program
{
private static readonly CancellationTokenSource TokenSource;
static Program()
{
TokenSource = new CancellationTokenSource();
Console.CancelKeyPress += (sender, args) =>
{
TokenSource.Cancel();
args.Cancel = true;
};
}
public static int Main(string[] args) =>
MainAsync(args, TokenSource.Token).ConfigureAwait(false).GetAwaiter().GetResult();
private static async Task<int> MainAsync(string[] args, CancellationToken ct)
{
ExecutionResult result;
var loggerFactory = new LoggerFactory()
.AddConsole(LogLevel.Trace, true);
var logger = loggerFactory.CreateLogger<Program>();
try
{
logger.LogDebug("Preparing the host builder");
using (var hostBuilder = new HostBuilder("HTTPMONITOR_ENVIRONMENT")
.UseLoggerFactory(loggerFactory)
.UseStartup<HttpMonitorStartup>()
.ConfigureConfigurationBuilder(p =>
{
p.Builder.AddCommandLine(args);
}))
{
await hostBuilder.RunHostAsync<HttpMonitorHost>(ct);
}
result = ExecutionResult.Success;
}
catch (TaskCanceledException)
{
logger.LogWarning("The application execution was canceled");
result = ExecutionResult.Canceled;
}
catch (Exception e)
{
logger.LogCritical(0, e, "Unexpected exception has occurred");
result = ExecutionResult.Failed;
}
logger.LogInformation("Application terminated [{result}]. Press <enter> to exit...", result);
Console.ReadLine();
return (int) result;
}
private enum ExecutionResult
{
Success = 0,
Failed = 1,
Canceled = 2
}
}
The ILoggerFactory
is initially created outside the builder because I want to include every log at least in the console. This isn't a requirement since if not set, the builder will use a default factory without any provider when building the host. I also used the environment variable HTTPMONITOR_ENVIRONMENT
to decide in which environment the host is running.
In this example, I used a custom IHostStartup
class to register and configure all the application dependencies. There are methods to add any supported configuration handler directly into the builder, but I preferred to keep the code inside the main method concise. Note that I append a second IConfigurationBuilder
handler to include the arguments into the configurations, but I could easily have passed them as a parameter to the startup class.
Then, I just run the host. If the class was not registered manually into the service collection, the builder will automatically add it with a scoped lifetime, so it will be contained by the IHostRunContext<THost>
instance.
When building a host, the pipeline is the following:
- Handlers for the
IConfigurationBuilder
, having access to the IHostingEnvironment
- Handlers for the
IConfigurationRoot
, having access to the IHostingEnvironment
- Handlers for the
ILoggerFactory
, having access to the IConfiguration
and IHostingEnvironment
- Handlers for the
IServiceCollection
, having access to the ILoggerFactory
, IConfiguration
and IHostingEnvironment
- Build the
IServiceProvider
, having access to the IServiceCollection
, ILoggerFactory
, IConfiguration
and IHostingEnvironment
- Handlers for the
IServiceProvider
, having access to the IServiceProvider
, ILoggerFactory
, IConfiguration
and IHostingEnvironment
Note that the instances ILoggerFactory
, IConfigurationRoot
and IHostingEnvironment
are automatically registered into the container.
Remarks: If you want to configure the logger factory, configurations or any other requirement and you need to know your current environment or directory and want to be sure you use the same properties as the host builder, you can always create HostingEnvironment
instances by using the constructor or the static
method HostingEnvironment.BuildDefault
and pass the environment as an argument for the builder. Example:
var loggerFactory = new LoggerFactory()
.AddConsole(LogLevel.Trace, true)
.AddNLog();
var env = HostingEnvironment.BuildDefault("HTTPMONITOR_ENVIRONMENT");
var nlogConfigFile = env.ContentRootFileProvider.GetFileInfo($"nlog.{env.Name}.config");
if (!nlogConfigFile.Exists)
nlogConfigFile = env.ContentRootFileProvider.GetFileInfo("nlog.config");
loggerFactory.ConfigureNLog(nlogConfigFile.PhysicalPath);
var builder = new HostBuilder(env).UseLoggerFactory(loggerFactory);
HttpMonitorStartup
Despite not being needed, a startup
class can be used to aggregate all the host setup:
public class HttpMonitorStartup : HostStartup
{
public override void ConfigureConfigurationBuilder(IConfigurationBuilderParam param)
{
param.Builder
.SetBasePath(param.Environment.ContentRootPath)
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{param.Environment.Name}.json", true, true)
.AddEnvironmentVariables();
}
public override void ConfigureLoggerFactory(ILoggerFactoryHandlerParam param)
{
var nlogConfigFile = param.Environment.ContentRootFileProvider.GetFileInfo
($"nlog.{param.Environment.Name}.config");
if (!nlogConfigFile.Exists)
nlogConfigFile = param.Environment.ContentRootFileProvider.GetFileInfo("nlog.config");
param.LoggerFactory
.AddNLog()
.ConfigureNLog(nlogConfigFile.PhysicalPath);
}
public override void ConfigureServiceCollection(IServiceCollectionHandlerParam param)
{
param.ServiceCollection
.AddOptions()
.Configure<HttpMonitorOptions>(param.Configuration)
.AddSingleton(s => s.GetRequiredService<IOptions<HttpMonitorOptions>>().Value);
param.ServiceCollection
.AddSingleton(s => new HttpClient())
.AddSingleton<IUrlRequester, UrlRequester>();
}
public override IServiceProvider BuildServiceProvider(IServiceProviderBuilderParam param)
{
var container = new ContainerBuilder();
container.Populate(param.ServiceCollection);
return new AutofacServiceProvider(container.Build());
}
}
Just like in ASP.NET Core, you can conditionally load settings, configure the application logging or replace the default container implementation. The abstract
class HostStartup
makes it easier to only override what is needed for your setup.
HttpMonitorHost
The application host running inside a scope of the dependency injection container, meaning you now have everything wired up just like any ASP.NET application. In this case, I just get the application options and start making the HTTP requests.
public class HttpMonitorHost : IHost
{
private readonly HttpMonitorOptions _options;
private readonly IUrlRequester _urlRequester;
private readonly ILogger<HttpMonitorHost> _logger;
public HttpMonitorHost
(HttpMonitorOptions options, IUrlRequester urlRequester, ILogger<HttpMonitorHost> logger)
{
_options = options;
_urlRequester = urlRequester;
_logger = logger;
}
public async Task RunAsync(CancellationToken ct)
{
for (var i = 0; i < _options.MaximumBatches; i++)
{
_logger.LogDebug("Sending a batch of requests");
await Task.WhenAll(_options.Urls.Select(url => _urlRequester.RequestAsync(url, ct)));
_logger.LogDebug("Waiting for the next batch time");
await Task.Delay(_options.BatchDelayInMs, ct);
}
_logger.LogDebug
("Maximum of {maximumBatches} batches have been made", _options.MaximumBatches);
}
}
Execution
When running the application, you should see an output as follows:
Conclusion
This article explained how the development of advanced console applications can be made using the package SimpleSoft.Hosting. Since it uses the most recent Microsoft extension packages, the support from third parties is great, making it an option to keep in mind.
History
- 2017-09-27: Initial version
- 2017-09-29: Added VS 2015 project and added explanation how to use the
HostingEnvironment
before creating the builder
- 2017-10-03: Added example image and updated code to most recent NuGets