Leveraging existing libraries to write a console application

Leveraging existing libraries to write a console application

Small console applications are handy and quite common in software projects to solve infrastructural problems which are not easy maintainable with shell scripts. For example I often use DbUp to migrate SQL databases. It is a great library, but provides only a library and no executable. So we tend to make dedicated console application in every project DbUpgets used. This is quite handy, it can be installed as a dotnet core tool, packed in a docker image or delivered with an Octopus Deploy package.

Every time we do that rather 'hacky' instead of having a proper code base for such kind of tools. I hate code which is not cleaned up, but has a lifetime of the whole project or product. This tend to suffer from the broken window effect and become worse and worse. So one day, I sat down and thought about how I can build a console application which is properly built and very easy to set up. You can get the sample code here.

I don't like to reinvent the wheel I was looking for libraries doing the job for me.

To have more control over the log output, I'm using Serilog. With Serilog we can write to the console, a file or wherever we want to send and store our log messages.

var levelSwitch = new LoggingLevelSwitch
{
    MinimumLevel = LogEventLevel.Information,
};

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.ControlledBy(levelSwitch)
    .Enrich.FromLogContext()
    .WriteTo.Console(outputTemplate: "{Timestamp:HH:mm:ss} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}")
    .CreateLogger();

In any normal application I would use a dependency injection container to leverage the inversion of control principle. Basically it does not matter which framework we use for that job. I just hooked on the dependency injection extension from Microsoft which are also used in Asp.Net Core. As you may recognize, I'm registering the logger with the Microsoft logging extension into the dependency injection container. This libraries play neatly together, which makes this easier for us.

var serviceProvider = new ServiceCollection()
    .AddLogging()
    .AddSingleton<ILoggerFactory>(_ => new SerilogLoggerFactory(Log.Logger))
    .BuildServiceProvider();

And that I can setup the CommanLineParser to parse the program arguments.

return await Parser.Default.ParseArguments<Option1, Option1>(args)
    .MapResult(
        // the command arguments are valid and we can do something here
        async (object options) =>
        {
            return Task.FromResult(0);
        },
        // the command was invalid, we return an error code
        _ => Task.FromResult(1));

Now almost everything is in place in order that we can work with it. The CommanLineParser requires you to implement annotated classes which are filled from the parser. I want to create a git commands like tool, so I'm using the Verb feature of the CommanLineParser.

[Verb("hello", HelpText = "Just an example command.")]
public class HelloCommand
{
    [Option('t', "target", Required = true, HelpText = "Specifies whom to say hello.")]
    public string GreetingTarget { get; set; }
}

[Verb("goodbye", HelpText = "Just an example command.")]
public class GoodbyeCommand
{
    [Option('t', "target", Required = true, HelpText = "Specifies whom to say hello.")]
    public string GreetingTarget { get; set; }
}

I need to register my command in the CommanLineParser.

return await Parser.Default.ParseArguments<HelloCommand, GoodbyeCommand>(args)
    .MapResult(
        async (object options) =>
        {
            return Task.FromResult(0);
        }

Oh wait, the map results is untyped. I need to distinguish between the different options classes and cast them in order to access their information. This could lead to nasty code with many if or switch blocks. I prefer to have some cleaner way to do that job. Luckily there is a software development pattern for this problem. The mediator pattern can help us calling the correct command logic according to our inputs. I'm using the MediatR library which is a fully managed implementation of that pattern. And because we are using a dependency injection container it comes with no overhead at all.

I'm registering the MediatR library in the dependency injection container.

var serviceProvider = new ServiceCollection()
    .AddLogging()
    .AddSingleton<ILoggerFactory>(_ => new SerilogLoggerFactory(Log.Logger))
    .AddMediatR(Assembly.GetExecutingAssembly())
    .BuildServiceProvider();

The commands must inherit from IRequest in order that it works together with the MediatR library. I'm creating a command base class for that, we also can use it later on to add some general command line arguments to the commands.

public abstract class CommandBase : IRequest<int>
{
}

Every command has a command handler, which executes the logic of the command. We can inject our dependencies into our command handlers. I'm only using the logger, but it can be anything you want. E.g. an Entity Framework context or whatever.

public class HelloCommandHandler : IRequestHandler<HelloCommand, int>
{
    private readonly ILogger<HelloCommandHandler> _logger;

    public HelloCommandHandler(ILogger<HelloCommandHandler> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public Task<int> Handle(HelloCommand request, CancellationToken cancellationToken)
    {
        _logger.LogInformation($"Hello {request.GreetingTarget}");

        return Task.FromResult(0);
    }
}

public class GoodbyeCommandHandler : IRequestHandler<GoodbyeCommand, int>
{
    private readonly ILogger<GoodbyeCommandHandler> _logger;

    public GoodbyeCommandHandler(ILogger<GoodbyeCommandHandler> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public Task<int> Handle(GoodbyeCommand request, CancellationToken cancellationToken)
    {
        _logger.LogInformation($"Goodbye {request.GreetingTarget}");

        return Task.FromResult(0);
    }
}

And finally we can call the MediatR with the command and this will execute the corresponding command handler.

return await Parser.Default.ParseArguments<HelloCommand, GoodbyeCommand>(args)
    .MapResult(
        async (CommandBase command) =>
        {
            return await serviceProvider.GetRequiredService<IMediator>().Send(command);
        },
        _ => Task.FromResult(1));

Cool! with that we have a clean command line tool which is easy to test and maintain.

As an addition, I'm adding some general arguments to the base class. To demonstrate this, I'm adding a Verbose flag which adjusts the current log level.

public abstract class CommandBase : IRequest<int>
{
    [Option("verbose", Default = false, Required = false, HelpText = "Enables verbose output.")]
    public bool Verbose { get; set; }
}

async (CommandBase command) =>
{
    if (command.Verbose)
    {
        levelSwitch.MinimumLevel = LogEventLevel.Verbose;
    }

    return await serviceProvider.GetRequiredService<IMediator>().Send(command);
},

I used this architecture already in a few projects and I must say it was worth to do it. It is easy to understand and it does not get messed up. At the end, every line of code brings in maintenance effort and can waist the time of the team. Just care a bit, even though it is not the main product you are creating helps you and all involved peoples.