Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating Advanced Console Applications in .NET

0.00/5 (No votes)
2 Oct 2017 1  
Adding support for Dependency Injection, Logging and Configurations in console applications using the package SimpleSoft.Hosting

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:

  1. Handlers for the IConfigurationBuilder, having access to the IHostingEnvironment
  2. Handlers for the IConfigurationRoot, having access to the IHostingEnvironment
  3. Handlers for the ILoggerFactory, having access to the IConfiguration and IHostingEnvironment
  4. Handlers for the IServiceCollection, having access to the ILoggerFactory, IConfiguration and IHostingEnvironment
  5. Build the IServiceProvider, having access to the IServiceCollection, ILoggerFactory, IConfiguration and IHostingEnvironment
  6. 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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here